diff --git a/README.md b/README.md
index 61945808..a59dbb94 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/
- Material Design
- Most Mastodon APIs implemented
+- Muti-Account support
- completely Open-source - no non-free dependencies like Google services
#### Head of development
diff --git a/app/build.gradle b/app/build.gradle
index 11bdc3cd..033afade 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
@@ -49,6 +50,7 @@ dependencies {
implementation('com.mikepenz:materialdrawer:6.0.4@aar') {
transitive = true
}
+ debugCompile 'im.dino:dbinspector:3.4.1@aar'
implementation "com.android.support:appcompat-v7:$supportLibraryVersion"
implementation "com.android.support:customtabs:$supportLibraryVersion"
implementation "com.android.support:recyclerview-v7:$supportLibraryVersion"
@@ -72,6 +74,9 @@ dependencies {
implementation 'android.arch.persistence.room:runtime:1.0.0'
kapt 'android.arch.persistence.room:compiler:1.0.0'
testImplementation 'junit:junit:4.12'
+
+ debugImplementation 'im.dino:dbinspector:3.4.1@aar'
+
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d7fb032..cf7119b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,7 +9,6 @@
-
. */
-
-package com.keylesspalace.tusky;
-
-import android.app.AlertDialog;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.support.customtabs.CustomTabsIntent;
-import android.support.v7.app.AppCompatActivity;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.keylesspalace.tusky.entity.AccessToken;
-import com.keylesspalace.tusky.entity.AppCredentials;
-import com.keylesspalace.tusky.network.MastodonApi;
-import com.keylesspalace.tusky.util.CustomTabsHelper;
-import com.keylesspalace.tusky.util.NotificationManager;
-import com.keylesspalace.tusky.util.OkHttpUtils;
-import com.keylesspalace.tusky.util.ResourcesUtils;
-import com.keylesspalace.tusky.util.ThemeUtils;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-import retrofit2.Retrofit;
-import retrofit2.converter.gson.GsonConverterFactory;
-
-public class LoginActivity extends AppCompatActivity {
- private static final String TAG = "LoginActivity"; // logging tag
- private static String OAUTH_SCOPES = "read write follow";
-
- private LinearLayout input;
- private LinearLayout loading;
- private EditText editText;
- private SharedPreferences preferences;
- private String domain;
- private String clientId;
- private String clientSecret;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- preferences = PreferenceManager.getDefaultSharedPreferences(this);
- String[] themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
- String appTheme = themeFlavorPair[0], themeFlavorPreference = themeFlavorPair[2];
-
- setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
-
- String flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
- if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT))
- flavor = themeFlavorPreference;
- ThemeUtils.setAppNightMode(flavor);
-
- setContentView(R.layout.activity_login);
-
- input = findViewById(R.id.login_input);
- loading = findViewById(R.id.login_loading);
- editText = findViewById(R.id.edit_text_domain);
- Button button = findViewById(R.id.button_login);
- TextView whatsAnInstance = findViewById(R.id.whats_an_instance);
-
- if (savedInstanceState != null) {
- domain = savedInstanceState.getString("domain");
- clientId = savedInstanceState.getString("clientId");
- clientSecret = savedInstanceState.getString("clientSecret");
- } else {
- domain = null;
- clientId = null;
- clientSecret = null;
- }
-
- preferences = getSharedPreferences(
- getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
-
- button.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- onButtonClick(editText);
- }
- });
-
- final Context context = this;
-
- whatsAnInstance.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- AlertDialog dialog = new AlertDialog.Builder(context)
- .setMessage(R.string.dialog_whats_an_instance)
- .setPositiveButton(R.string.action_close,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- })
- .show();
- TextView textView = dialog.findViewById(android.R.id.message);
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- });
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- outState.putString("domain", domain);
- outState.putString("clientId", clientId);
- outState.putString("clientSecret", clientSecret);
- super.onSaveInstanceState(outState);
- }
-
- /** Make sure the user-entered text is just a fully-qualified domain name. */
- @NonNull
- private static String validateDomain(String s) {
- // Strip any schemes out.
- s = s.replaceFirst("http://", "");
- s = s.replaceFirst("https://", "");
- // If a username was included (e.g. username@example.com), just take what's after the '@'.
- int at = s.lastIndexOf('@');
- if (at != -1) {
- s = s.substring(at + 1);
- }
- return s.trim();
- }
-
- private String getOauthRedirectUri() {
- String scheme = getString(R.string.oauth_scheme);
- String host = BuildConfig.APPLICATION_ID;
- return scheme + "://" + host + "/";
- }
-
- private MastodonApi getApiFor(String domain) {
- Retrofit retrofit = new Retrofit.Builder()
- .baseUrl("https://" + domain)
- .client(OkHttpUtils.getCompatibleClient(preferences))
- .addConverterFactory(GsonConverterFactory.create())
- .build();
-
- return retrofit.create(MastodonApi.class);
- }
-
- /**
- * Obtain the oauth client credentials for this app. This is only necessary the first time the
- * app is run on a given server instance. So, after the first authentication, they are
- * saved in SharedPreferences and every subsequent run they are simply fetched from there.
- */
- private void onButtonClick(final EditText editText) {
- domain = validateDomain(editText.getText().toString());
- /* Attempt to get client credentials from SharedPreferences, and if not present
- * (such as in the case that the domain has never been accessed before)
- * authenticate with the server and store the received credentials to use next
- * time. */
- String prefClientId = preferences.getString(domain + "/client_id", null);
- String prefClientSecret = preferences.getString(domain + "/client_secret", null);
-
- if (prefClientId != null && prefClientSecret != null) {
- clientId = prefClientId;
- clientSecret = prefClientSecret;
- redirectUserToAuthorizeAndLogin(editText);
- } else {
- Callback callback = new Callback() {
- @Override
- public void onResponse(@NonNull Call call,
- @NonNull Response response) {
- if (!response.isSuccessful()) {
- editText.setError(getString(R.string.error_failed_app_registration));
- Log.e(TAG, "App authentication failed. " + response.message());
- return;
- }
- AppCredentials credentials = response.body();
- clientId = credentials.clientId;
- clientSecret = credentials.clientSecret;
- preferences.edit()
- .putString(domain + "/client_id", clientId)
- .putString(domain + "/client_secret", clientSecret)
- .apply();
- redirectUserToAuthorizeAndLogin(editText);
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- editText.setError(getString(R.string.error_failed_app_registration));
- Log.e(TAG, Log.getStackTraceString(t));
- }
- };
-
- try {
- getApiFor(domain)
- .authenticateApp(getString(R.string.app_name), getOauthRedirectUri(),
- OAUTH_SCOPES, getString(R.string.app_website))
- .enqueue(callback);
- } catch (IllegalArgumentException e) {
- editText.setError(getString(R.string.error_invalid_domain));
- }
- }
- }
-
- /**
- * Chain together the key-value pairs into a query string, for either appending to a URL or
- * as the content of an HTTP request.
- */
- @NonNull
- private static String toQueryString(Map parameters) {
- StringBuilder s = new StringBuilder();
- String between = "";
- for (Map.Entry entry : parameters.entrySet()) {
- s.append(between);
- s.append(Uri.encode(entry.getKey()));
- s.append("=");
- s.append(Uri.encode(entry.getValue()));
- between = "&";
- }
- return s.toString();
- }
-
- private static boolean openInCustomTab(Uri uri, Context context) {
- int toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar");
-
- CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
- builder.setToolbarColor(toolbarColor);
- CustomTabsIntent customTabsIntent = builder.build();
- try {
- String packageName = CustomTabsHelper.getPackageNameToUse(context);
- /* If we cant find a package name, it means theres no browser that supports
- * Chrome Custom Tabs installed. So, we fallback to the webview */
- if (packageName == null) {
- return false;
- } else {
- customTabsIntent.intent.setPackage(packageName);
- customTabsIntent.launchUrl(context, uri);
- }
- } catch (ActivityNotFoundException e) {
- Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
- return false;
- }
- return true;
- }
-
- private void redirectUserToAuthorizeAndLogin(EditText editText) {
- /* To authorize this app and log in it's necessary to redirect to the domain given,
- * activity_login there, and the server will redirect back to the app with its response. */
- String endpoint = MastodonApi.ENDPOINT_AUTHORIZE;
- String redirectUri = getOauthRedirectUri();
- Map parameters = new HashMap<>();
- parameters.put("client_id", clientId);
- parameters.put("redirect_uri", redirectUri);
- parameters.put("response_type", "code");
- parameters.put("scope", OAUTH_SCOPES);
- String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
- Uri uri = Uri.parse(url);
- if (!openInCustomTab(uri, this)) {
- Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri);
- if (viewIntent.resolveActivity(getPackageManager()) != null) {
- startActivity(viewIntent);
- } else {
- editText.setError(getString(R.string.error_no_web_browser_found));
- }
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- if (domain != null) {
- preferences.edit()
- .putString("domain", domain)
- .putString("clientId", clientId)
- .putString("clientSecret", clientSecret)
- .apply();
- }
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- /* Check if we are resuming during authorization by seeing if the intent contains the
- * redirect that was given to the server. If so, its response is here! */
- Uri uri = getIntent().getData();
- String redirectUri = getOauthRedirectUri();
-
- preferences = getSharedPreferences(
- getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
-
- if (preferences.getString("accessToken", null) != null
- && preferences.getString("domain", null) != null) {
- // We are already logged in, go to MainActivity
- Intent intent = new Intent(this, MainActivity.class);
- startActivity(intent);
- finish();
- return;
- }
-
- if (uri != null && uri.toString().startsWith(redirectUri)) {
- // This should either have returned an authorization code or an error.
- String code = uri.getQueryParameter("code");
- String error = uri.getQueryParameter("error");
-
- if (code != null) {
- /* During the redirect roundtrip this Activity usually dies, which wipes out the
- * instance variables, so they have to be recovered from where they were saved in
- * SharedPreferences. */
- domain = preferences.getString("domain", null);
- clientId = preferences.getString("clientId", null);
- clientSecret = preferences.getString("clientSecret", null);
-
- setLoading(true);
- /* Since authorization has succeeded, the final step to log in is to exchange
- * the authorization code for an access token. */
- Callback callback = new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.isSuccessful()) {
- onLoginSuccess(response.body().accessToken);
- } else {
- setLoading(false);
-
- editText.setError(getString(R.string.error_retrieving_oauth_token));
- Log.e(TAG, String.format("%s %s",
- getString(R.string.error_retrieving_oauth_token),
- response.message()));
- }
- }
-
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- setLoading(false);
- editText.setError(getString(R.string.error_retrieving_oauth_token));
- Log.e(TAG, String.format("%s %s",
- getString(R.string.error_retrieving_oauth_token),
- t.getMessage()));
- }
- };
-
- getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code,
- "authorization_code").enqueue(callback);
- } else if (error != null) {
- /* Authorization failed. Put the error response where the user can read it and they
- * can try again. */
- setLoading(false);
- editText.setError(getString(R.string.error_authorization_denied));
- Log.e(TAG, getString(R.string.error_authorization_denied) + error);
- } else {
- setLoading(false);
- // This case means a junk response was received somehow.
- editText.setError(getString(R.string.error_authorization_unknown));
- }
- }
- }
-
- private void setLoading(boolean loadingState) {
- if (loadingState) {
- loading.setVisibility(View.VISIBLE);
- input.setVisibility(View.GONE);
- } else {
- loading.setVisibility(View.GONE);
- input.setVisibility(View.VISIBLE);
- }
- }
-
- private void onLoginSuccess(String accessToken) {
- boolean committed = preferences.edit()
- .putString("domain", domain)
- .putString("accessToken", accessToken)
- .commit();
- if (!committed) {
- setLoading(false);
- editText.setError(getString(R.string.error_retrieving_oauth_token));
- return;
- }
-
- //create notification channels ahead of time so users can edit the settings
- NotificationManager.createNotificationChannels(this);
-
- Intent intent = new Intent(this, MainActivity.class);
- startActivity(intent);
- finish();
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt
new file mode 100644
index 00000000..f50565f2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt
@@ -0,0 +1,377 @@
+/* Copyright 2017 Andrew Dawson
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky
+
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.net.Uri
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.support.customtabs.CustomTabsIntent
+import android.support.v7.app.AppCompatActivity
+import android.text.method.LinkMovementMethod
+import android.util.Log
+import android.view.MenuItem
+import android.view.View
+import android.widget.EditText
+import android.widget.TextView
+import com.keylesspalace.tusky.entity.AccessToken
+import com.keylesspalace.tusky.entity.AppCredentials
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.CustomTabsHelper
+import com.keylesspalace.tusky.util.OkHttpUtils
+import com.keylesspalace.tusky.util.ResourcesUtils
+import com.keylesspalace.tusky.util.ThemeUtils
+import kotlinx.android.synthetic.main.activity_login.*
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+
+class LoginActivity : AppCompatActivity() {
+
+ private lateinit var preferences: SharedPreferences
+ private var domain: String = ""
+ private var clientId: String? = null
+ private var clientSecret: String? = null
+
+ private val oauthRedirectUri: String
+ get() {
+ val scheme = getString(R.string.oauth_scheme)
+ val host = BuildConfig.APPLICATION_ID
+ return "$scheme://$host/"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ preferences = PreferenceManager.getDefaultSharedPreferences(this)
+ val themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT)!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val appTheme = themeFlavorPair[0]
+ val themeFlavorPreference = themeFlavorPair[2]
+
+ setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme))
+
+ var flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT)
+ if (flavor == ThemeUtils.THEME_FLAVOR_DEFAULT)
+ flavor = themeFlavorPreference
+ ThemeUtils.setAppNightMode(flavor)
+
+ setContentView(R.layout.activity_login)
+
+ if (savedInstanceState != null) {
+ domain = savedInstanceState.getString(DOMAIN)
+ clientId = savedInstanceState.getString(CLIENT_ID)
+ clientSecret = savedInstanceState.getString(CLIENT_SECRET)
+ }
+
+ preferences = getSharedPreferences(
+ getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
+
+ loginButton.setOnClickListener { onButtonClick() }
+
+ whatsAnInstanceTextView.setOnClickListener {
+ val dialog = AlertDialog.Builder(this)
+ .setMessage(R.string.dialog_whats_an_instance)
+ .setPositiveButton(R.string.action_close, null)
+ .show()
+ val textView = dialog.findViewById(android.R.id.message)
+ textView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ if(isAdditionalLogin()) {
+ setSupportActionBar(toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowTitleEnabled(false)
+ } else {
+ toolbar.visibility = View.GONE
+ }
+
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if(item.itemId == android.R.id.home) {
+ onBackPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ outState.putString(DOMAIN, domain)
+ outState.putString(CLIENT_ID, clientId)
+ outState.putString(CLIENT_SECRET, clientSecret)
+ super.onSaveInstanceState(outState)
+ }
+
+ private fun getApiFor(domain: String): MastodonApi {
+ val retrofit = Retrofit.Builder()
+ .baseUrl("https://" + domain)
+ .client(OkHttpUtils.getCompatibleClient(preferences))
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+ return retrofit.create(MastodonApi::class.java)
+ }
+
+ /**
+ * Obtain the oauth client credentials for this app. This is only necessary the first time the
+ * app is run on a given server instance. So, after the first authentication, they are
+ * saved in SharedPreferences and every subsequent run they are simply fetched from there.
+ */
+ private fun onButtonClick() {
+
+ loginButton.isEnabled = false
+
+ domain = validateDomain(domainEditText.text.toString())
+
+ val callback = object : Callback {
+ override fun onResponse(call: Call,
+ response: Response) {
+ if (!response.isSuccessful) {
+ loginButton.isEnabled = true
+ domainEditText.error = getString(R.string.error_failed_app_registration)
+ Log.e(TAG, "App authentication failed. " + response.message())
+ return
+ }
+ val credentials = response.body()
+ clientId = credentials!!.clientId
+ clientSecret = credentials.clientSecret
+
+ redirectUserToAuthorizeAndLogin(domainEditText)
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ loginButton.isEnabled = true
+ domainEditText.error = getString(R.string.error_failed_app_registration)
+ setLoading(false)
+ Log.e(TAG, Log.getStackTraceString(t))
+ }
+ }
+
+ try {
+ getApiFor(domain)
+ .authenticateApp(getString(R.string.app_name), oauthRedirectUri,
+ OAUTH_SCOPES, getString(R.string.app_website))
+ .enqueue(callback)
+ setLoading(true)
+ } catch (e: IllegalArgumentException) {
+ setLoading(false)
+ domainEditText.error = getString(R.string.error_invalid_domain)
+ }
+
+ }
+
+ private fun redirectUserToAuthorizeAndLogin(editText: EditText) {
+ /* To authorize this app and log in it's necessary to redirect to the domain given,
+ * activity_login there, and the server will redirect back to the app with its response. */
+ val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
+ val redirectUri = oauthRedirectUri
+ val parameters = HashMap()
+ parameters["client_id"] = clientId!!
+ parameters["redirect_uri"] = redirectUri
+ parameters["response_type"] = "code"
+ parameters["scope"] = OAUTH_SCOPES
+ val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
+ val uri = Uri.parse(url)
+ if (!openInCustomTab(uri, this)) {
+ val viewIntent = Intent(Intent.ACTION_VIEW, uri)
+ if (viewIntent.resolveActivity(packageManager) != null) {
+ startActivity(viewIntent)
+ } else {
+ editText.error = getString(R.string.error_no_web_browser_found)
+ setLoading(false)
+ }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ preferences.edit()
+ .putString("domain", domain)
+ .putString("clientId", clientId)
+ .putString("clientSecret", clientSecret)
+ .apply()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ /* Check if we are resuming during authorization by seeing if the intent contains the
+ * redirect that was given to the server. If so, its response is here! */
+ val uri = intent.data
+ val redirectUri = oauthRedirectUri
+
+ if (uri != null && uri.toString().startsWith(redirectUri)) {
+ // This should either have returned an authorization code or an error.
+ val code = uri.getQueryParameter("code")
+ val error = uri.getQueryParameter("error")
+
+ if (code != null) {
+ /* During the redirect roundtrip this Activity usually dies, which wipes out the
+ * instance variables, so they have to be recovered from where they were saved in
+ * SharedPreferences. */
+ domain = preferences.getString(DOMAIN, null)
+ clientId = preferences.getString(CLIENT_ID, null)
+ clientSecret = preferences.getString(CLIENT_SECRET, null)
+
+ setLoading(true)
+ /* Since authorization has succeeded, the final step to log in is to exchange
+ * the authorization code for an access token. */
+ val callback = object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ if (response.isSuccessful) {
+ onLoginSuccess(response.body()!!.accessToken)
+ } else {
+ setLoading(false)
+ domainEditText.error = getString(R.string.error_retrieving_oauth_token)
+ Log.e(TAG, String.format("%s %s",
+ getString(R.string.error_retrieving_oauth_token),
+ response.message()))
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ setLoading(false)
+ domainEditText.error = getString(R.string.error_retrieving_oauth_token)
+ Log.e(TAG, String.format("%s %s",
+ getString(R.string.error_retrieving_oauth_token),
+ t.message))
+ }
+ }
+
+ getApiFor(domain).fetchOAuthToken(clientId, clientSecret, redirectUri, code,
+ "authorization_code").enqueue(callback)
+ } else if (error != null) {
+ /* Authorization failed. Put the error response where the user can read it and they
+ * can try again. */
+ setLoading(false)
+ domainEditText.error = getString(R.string.error_authorization_denied)
+ Log.e(TAG, String.format("%s %s",
+ getString(R.string.error_authorization_denied),
+ error))
+ } else {
+ // This case means a junk response was received somehow.
+ setLoading(false)
+ domainEditText.error = getString(R.string.error_authorization_unknown)
+ }
+ } else {
+ // first show or user cancelled login
+ setLoading(false)
+ }
+ }
+
+ private fun setLoading(loadingState: Boolean) {
+ if (loadingState) {
+ loginLoadingLayout.visibility = View.VISIBLE
+ loginInputLayout.visibility = View.GONE
+ } else {
+ loginLoadingLayout.visibility = View.GONE
+ loginInputLayout.visibility = View.VISIBLE
+ loginButton.isEnabled = true
+ }
+ }
+
+ private fun isAdditionalLogin() : Boolean {
+ return intent.getBooleanExtra(LOGIN_MODE, false)
+ }
+
+ private fun onLoginSuccess(accessToken: String) {
+
+ setLoading(true)
+
+ TuskyApplication.getAccountManager().addAccount(accessToken, domain)
+
+ val intent = Intent(this, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ finish()
+ }
+
+ companion object {
+ private const val TAG = "LoginActivity" // logging tag
+ private const val OAUTH_SCOPES = "read write follow"
+ private const val LOGIN_MODE = "LOGIN_MODE"
+ private const val DOMAIN = "domain"
+ private const val CLIENT_ID = "clientId"
+ private const val CLIENT_SECRET = "clientSecret"
+
+ @JvmStatic
+ fun getIntent(context: Context, mode: Boolean): Intent {
+ val loginIntent = Intent(context, LoginActivity::class.java)
+ loginIntent.putExtra(LOGIN_MODE, mode)
+ return loginIntent
+ }
+
+ /** Make sure the user-entered text is just a fully-qualified domain name. */
+ private fun validateDomain(domain: String): String {
+ // Strip any schemes out.
+ var s = domain.replaceFirst("http://", "")
+ s = s.replaceFirst("https://", "")
+ // If a username was included (e.g. username@example.com), just take what's after the '@'.
+ val at = s.lastIndexOf('@')
+ if (at != -1) {
+ s = s.substring(at + 1)
+ }
+ return s.trim { it <= ' ' }
+ }
+
+ /**
+ * Chain together the key-value pairs into a query string, for either appending to a URL or
+ * as the content of an HTTP request.
+ */
+ private fun toQueryString(parameters: Map): String {
+ val s = StringBuilder()
+ var between = ""
+ for ((key, value) in parameters) {
+ s.append(between)
+ s.append(Uri.encode(key))
+ s.append("=")
+ s.append(Uri.encode(value))
+ between = "&"
+ }
+ return s.toString()
+ }
+
+ private fun openInCustomTab(uri: Uri, context: Context): Boolean {
+
+ val toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar")
+ val builder = CustomTabsIntent.Builder()
+ builder.setToolbarColor(toolbarColor)
+ val customTabsIntent = builder.build()
+ try {
+ val packageName = CustomTabsHelper.getPackageNameToUse(context)
+ /* If we cant find a package name, it means theres no browser that supports
+ * Chrome Custom Tabs installed. So, we fallback to the webview */
+ if (packageName == null) {
+ return false
+ } else {
+ customTabsIntent.intent.`package` = packageName
+ customTabsIntent.launchUrl(context, uri)
+ }
+ } catch (e: ActivityNotFoundException) {
+ Log.w(TAG, "Activity was not found for intent, " + customTabsIntent.toString())
+ return false
+ }
+
+ return true
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
index aed7abe9..7b69ba56 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
@@ -33,10 +33,11 @@ import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.KeyEvent;
-import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
+import com.keylesspalace.tusky.db.AccountEntity;
+import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
@@ -51,6 +52,7 @@ import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
+import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
@@ -67,6 +69,7 @@ import retrofit2.Response;
public class MainActivity extends BaseActivity implements ActionButtonActivity {
private static final String TAG = "MainActivity"; // logging tag
+ private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
private static final long DRAWER_ITEM_EDIT_PROFILE = 0;
private static final long DRAWER_ITEM_FAVOURITES = 1;
private static final long DRAWER_ITEM_MUTED_USERS = 2;
@@ -82,14 +85,32 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
private static int COMPOSE_RESULT = 1;
private FloatingActionButton composeButton;
- private String loggedInAccountId;
- private String loggedInAccountUsername;
private AccountHeader headerResult;
private Drawer drawer;
private ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
+
+ // account switching has to be done before MastodonApi is created in super.onCreate
+ Intent intent = getIntent();
+
+ int tabPosition = 0;
+
+ if (intent != null) {
+ long accountId = intent.getLongExtra(NotificationManager.ACCOUNT_ID, -1);
+
+ if(accountId != -1) {
+ // user clicked a notification, show notification tab and switch user if necessary
+ tabPosition = 1;
+ AccountEntity account = TuskyApplication.getAccountManager().getActiveAccount();
+
+ if (account == null || accountId != account.getId()) {
+ TuskyApplication.getAccountManager().setActiveAccount(accountId);
+ }
+ }
+ }
+
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@@ -99,8 +120,8 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
viewPager = findViewById(R.id.pager);
floatingBtn.setOnClickListener(v -> {
- Intent intent = new Intent(getApplicationContext(), ComposeActivity.class);
- startActivityForResult(intent, COMPOSE_RESULT);
+ Intent composeIntent = new Intent(getApplicationContext(), ComposeActivity.class);
+ startActivityForResult(composeIntent, COMPOSE_RESULT);
});
setupDrawer();
@@ -109,7 +130,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
ThemeUtils.setDrawableTint(this, drawerToggle.getDrawable(), R.attr.toolbar_icon_tint);
drawerToggle.setOnClickListener(v -> drawer.openDrawer());
- /* Fetch user info while we're doing other things. This has to be after setting up the
+ /* Fetch user info while we're doing other things. This has to be done after setting up the
* drawer, though, because its callback touches the header in the drawer. */
fetchUserInfo();
@@ -143,6 +164,15 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
tab.setContentDescription(pageTitles[i]);
}
+ if (tabPosition != 0) {
+ TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
+ if (tab != null) {
+ tab.select();
+ } else {
+ tabPosition = 0;
+ }
+ }
+
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
@@ -151,7 +181,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
tintTab(tab, true);
if(tab.getPosition() == 1) {
- NotificationManager.clearNotifications(MainActivity.this);
+ NotificationManager.clearNotificationsForActiveAccount(MainActivity.this);
}
}
@@ -161,29 +191,15 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
}
@Override
- public void onTabReselected(TabLayout.Tab tab) {
- }
+ public void onTabReselected(TabLayout.Tab tab) { }
});
- Intent intent = getIntent();
-
- int tabSelected = 0;
- if (intent != null) {
- int tabPosition = intent.getIntExtra("tab_position", 0);
- if (tabPosition != 0) {
- TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
- if (tab != null) {
- tab.select();
- tabSelected = tabPosition;
- }
- }
- }
for (int i = 0; i < 4; i++) {
- tintTab(tabLayout.getTabAt(i), i == tabSelected);
+ tintTab(tabLayout.getTabAt(i), i == tabPosition);
}
// Setup push notifications
- if (arePushNotificationsEnabled()) {
+ if (TuskyApplication.getAccountManager().notificationsEnabled()) {
enablePushNotifications();
} else {
disablePushNotifications();
@@ -196,7 +212,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
protected void onResume() {
super.onResume();
- NotificationManager.clearNotifications(this);
+ NotificationManager.clearNotificationsForActiveAccount(this);
/* After editing a profile, the profile header in the navigation drawer needs to be
* refreshed */
@@ -208,9 +224,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
.apply();
}
- if(viewPager.getCurrentItem() == 1) {
- NotificationManager.clearNotifications(this);
- }
}
@Override
@@ -267,28 +280,18 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
private void setupDrawer() {
headerResult = new AccountHeaderBuilder()
.withActivity(this)
- .withSelectionListEnabledForSingleProfile(false)
.withDividerBelowHeader(false)
.withHeaderBackgroundScaleType(ImageView.ScaleType.CENTER_CROP)
- .withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
- @Override
- public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
- if (current && loggedInAccountId != null) {
- Intent intent = new Intent(MainActivity.this, AccountActivity.class);
- intent.putExtra("id", loggedInAccountId);
- startActivity(intent);
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onProfileImageLongClick(View view, IProfile profile, boolean current) {
- return false;
- }
- })
- .withCompactStyle(true)
+ .withCurrentProfileHiddenInList(true)
+ .withOnAccountHeaderListener((view, profile, current) -> handleProfileClick(profile, current))
+ .addProfiles(
+ new ProfileSettingDrawerItem()
+ .withIdentifier(DRAWER_ITEM_ADD_ACCOUNT)
+ .withName(R.string.add_account_name)
+ .withDescription(R.string.add_account_description)
+ .withIcon(GoogleMaterial.Icon.gmd_add))
.build();
+
headerResult.getView()
.findViewById(R.id.material_drawer_account_header_current)
.setContentDescription(getString(R.string.action_view_profile));
@@ -371,6 +374,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
startActivity(ListsActivity.newIntent(this));
}
+
}
return false;
@@ -388,43 +392,78 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
}
}
+ private boolean handleProfileClick(IProfile profile, boolean current) {
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
+
+ //open profile when active image was clicked
+ if (current && activeAccount != null) {
+ Intent intent = new Intent(MainActivity.this, AccountActivity.class);
+ intent.putExtra("id", activeAccount.getAccountId());
+ startActivity(intent);
+ return true;
+ }
+ //open LoginActivity to add new account
+ if(profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT ) {
+ startActivity(LoginActivity.getIntent(this, true));
+ return true;
+ }
+ //change Account
+ changeAccount(profile.getIdentifier());
+ return false;
+ }
+
+
+ private void changeAccount(long newSelectedId) {
+ TuskyApplication.getAccountManager().setActiveAccount(newSelectedId);
+
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ finish();
+
+ overridePendingTransition(R.anim.explode, R.anim.explode);
+ }
+
private void logout() {
- new AlertDialog.Builder(this)
- .setTitle(R.string.action_logout)
- .setMessage(R.string.action_logout_confirm)
- .setPositiveButton(android.R.string.yes, (dialog, which) -> {
- if (arePushNotificationsEnabled()) disablePushNotifications();
- getPrivatePreferences().edit()
- .remove("domain")
- .remove("accessToken")
- .remove("appAccountId")
- .apply();
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
- Intent intent = new Intent(MainActivity.this, LoginActivity.class);
- startActivity(intent);
- finish();
- })
- .setNegativeButton(android.R.string.no, null)
- .show();
+ if(activeAccount != null) {
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.action_logout)
+ .setMessage(getString(R.string.action_logout_confirm, activeAccount.getFullName()))
+ .setPositiveButton(android.R.string.yes, (dialog, which) -> {
+
+ AccountManager accountManager = TuskyApplication.getAccountManager();
+
+ NotificationManager.deleteNotificationChannelsForAccount(accountManager.getActiveAccount(), MainActivity.this);
+
+ AccountEntity newAccount = accountManager.logActiveAccountOut();
+
+ if (!accountManager.notificationsEnabled()) disablePushNotifications();
+
+ Intent intent;
+ if (newAccount == null) {
+ intent = LoginActivity.getIntent(MainActivity.this, false);
+ } else {
+ intent = new Intent(MainActivity.this, MainActivity.class);
+ }
+ startActivity(intent);
+ finish();
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
}
private void fetchUserInfo() {
- SharedPreferences preferences = getPrivatePreferences();
- final String domain = preferences.getString("domain", null);
- String id = preferences.getString("loggedInAccountId", null);
- String username = preferences.getString("loggedInAccountUsername", null);
-
- if (id != null && username != null) {
- loggedInAccountId = id;
- loggedInAccountUsername = username;
- }
mastodonApi.accountVerifyCredentials().enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
- onFetchUserInfoSuccess(response.body(), domain);
+ onFetchUserInfoSuccess(response.body());
} else {
onFetchUserInfoFailure(new Exception(response.message()));
}
@@ -437,22 +476,34 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
});
}
- private void onFetchUserInfoSuccess(Account me, String domain) {
+ private void onFetchUserInfoSuccess(Account me) {
// Add the header image and avatar from the account, into the navigation drawer header.
ImageView background = headerResult.getHeaderBackgroundView();
+ background.setColorFilter(ContextCompat.getColor(this, R.color.header_background_filter));
background.setBackgroundColor(ContextCompat.getColor(this, R.color.window_background_dark));
Picasso.with(MainActivity.this)
.load(me.header)
.placeholder(R.drawable.account_header_default)
.into(background);
- headerResult.clear();
- headerResult.addProfiles(
- new ProfileDrawerItem()
- .withName(me.getDisplayName())
- .withEmail(String.format("%s@%s", me.username, domain))
- .withIcon(me.avatar)
- );
+ AccountManager am = TuskyApplication.getAccountManager();
+
+ am.updateActiveAccount(me);
+
+ NotificationManager.createNotificationChannelsForAccount(am.getActiveAccount(), this);
+
+ List allAccounts = am.getAllAccountsOrderedByActive();
+
+ for(AccountEntity acc: allAccounts) {
+ headerResult.addProfiles(
+ new ProfileDrawerItem()
+ .withName(acc.getDisplayName())
+ .withIcon(acc.getProfilePictureUrl())
+ .withNameShown(true)
+ .withIdentifier(acc.getId())
+ .withEmail(acc.getFullName()));
+
+ }
// Show follow requests in the menu, if this is a locked account.
if (me.locked) {
@@ -464,14 +515,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
drawer.addItemAtPosition(followRequestsItem, 3);
}
- // Update the current login information.
- loggedInAccountId = me.id;
- loggedInAccountUsername = me.username;
- getPrivatePreferences().edit()
- .putString("loggedInAccountId", loggedInAccountId)
- .putString("loggedInAccountUsername", loggedInAccountUsername)
- .putBoolean("loggedInAccountLocked", me.locked)
- .apply();
}
private void onFetchUserInfoFailure(Exception exception) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java b/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java
index 3b35255d..b1b775cb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java
+++ b/app/src/main/java/com/keylesspalace/tusky/NotificationPullJobCreator.java
@@ -20,23 +20,23 @@ import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
+import android.util.Log;
import com.evernote.android.job.Job;
import com.evernote.android.job.JobCreator;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
-import com.keylesspalace.tusky.network.AuthInterceptor;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.NotificationManager;
import com.keylesspalace.tusky.util.OkHttpUtils;
import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
+import java.math.BigInteger;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import okhttp3.OkHttpClient;
import retrofit2.Response;
@@ -49,7 +49,8 @@ import retrofit2.converter.gson.GsonConverterFactory;
public final class NotificationPullJobCreator implements JobCreator {
- public static final int NOTIFY_ID = 6; // chosen by fair dice roll, guaranteed to be random
+ private static final String TAG = "NotificationPJC";
+
static final String NOTIFICATIONS_JOB_TAG = "notifications_job_tag";
private Context context;
@@ -62,15 +63,7 @@ public final class NotificationPullJobCreator implements JobCreator {
@Override
public Job create(@NonNull String tag) {
if (tag.equals(NOTIFICATIONS_JOB_TAG)) {
- SharedPreferences preferences = context.getSharedPreferences(
- context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- final String domain = preferences.getString("domain", null);
-
- if(domain == null) {
- return null;
- } else {
- return new NotificationPullJob(domain, context);
- }
+ return new NotificationPullJob(context);
}
return null;
}
@@ -80,7 +73,6 @@ public final class NotificationPullJobCreator implements JobCreator {
context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder(preferences)
- .addInterceptor(new AuthInterceptor(context))
.build();
Gson gson = new GsonBuilder()
@@ -98,48 +90,72 @@ public final class NotificationPullJobCreator implements JobCreator {
private final static class NotificationPullJob extends Job {
- @NonNull private MastodonApi mastodonApi;
private Context context;
- NotificationPullJob(String domain, Context context) {
- this.mastodonApi = createMastodonApi(domain, context);
+ NotificationPullJob(Context context) {
this.context = context;
}
@NonNull
@Override
protected Result onRunJob(Params params) {
- try {
- Response> notifications =
- mastodonApi.notifications(null, null, null).execute();
- if (notifications.isSuccessful()) {
- onNotificationsReceived(notifications.body());
- } else {
- return Result.FAILURE;
+
+ List accountList = new ArrayList<>(TuskyApplication.getAccountManager().getAllAccountsOrderedByActive());
+
+ for(AccountEntity account: accountList) {
+
+ if(account.getNotificationsEnabled()) {
+ MastodonApi api = createMastodonApi(account.getDomain(), context);
+ try {
+ Log.d(TAG, "getting Notifications for "+account.getFullName());
+ Response> notifications =
+ api.notificationsWithAuth(String.format("Bearer %s", account.getAccessToken())).execute();
+ if (notifications.isSuccessful()) {
+ onNotificationsReceived(account, notifications.body());
+ } else {
+ Log.w(TAG, "error receiving notificationsEnabled");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "error receiving notificationsEnabled", e);
+ }
}
- } catch (IOException e) {
- e.printStackTrace();
- return Result.FAILURE;
+
}
+
return Result.SUCCESS;
+
+
}
- private void onNotificationsReceived(List notificationList) {
- SharedPreferences notificationsPreferences = context.getSharedPreferences(
- "Notifications", Context.MODE_PRIVATE);
- //make a copy of the string set, the returned instance should not be modified
- Set currentIds = new HashSet<>(notificationsPreferences.getStringSet(
- "current_ids", Collections.emptySet()));
- for (Notification notification : notificationList) {
- String id = notification.id;
- if (!currentIds.contains(id)) {
- currentIds.add(id);
- NotificationManager.make(context, NOTIFY_ID, notification);
+ private void onNotificationsReceived(AccountEntity account, List notificationList) {
+
+ BigInteger newId = new BigInteger(account.getLastNotificationId());
+
+ BigInteger newestId = BigInteger.ZERO;
+
+ for(Notification notification: notificationList){
+
+ BigInteger currentId = new BigInteger(notification.id);
+
+ if(isBiggerThan(currentId, newestId)) {
+ newestId = currentId;
+ }
+
+ if (isBiggerThan(currentId, newId)) {
+ account.setLastNotificationId(notification.id);
+
+ NotificationManager.make(context, notification, account);
}
}
- notificationsPreferences.edit()
- .putStringSet("current_ids", currentIds)
- .apply();
+
+ account.setLastNotificationId(newestId.toString());
+ TuskyApplication.getAccountManager().saveAccount(account);
+
+ }
+
+ private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
+
+ return lastShownNotificationId.compareTo(newId) == - 1;
}
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java
index 9e1b732f..2445111e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.java
@@ -15,12 +15,13 @@
package com.keylesspalace.tusky;
-import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
+import com.keylesspalace.tusky.db.AccountEntity;
+import com.keylesspalace.tusky.util.NotificationManager;
+
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -28,16 +29,16 @@ public class SplashActivity extends AppCompatActivity {
/* Determine whether the user is currently logged in, and if so go ahead and load the
* timeline. Otherwise, start the activity_login screen. */
- SharedPreferences preferences = getSharedPreferences(
- getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- String domain = preferences.getString("domain", null);
- String accessToken = preferences.getString("accessToken", null);
+
+ NotificationManager.deleteLegacyNotificationChannels(this);
+
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
Intent intent;
- if (domain != null && accessToken != null) {
+ if (activeAccount != null) {
intent = new Intent(this, MainActivity.class);
} else {
- intent = new Intent(this, LoginActivity.class);
+ intent = LoginActivity.getIntent(this, false);
}
startActivity(intent);
finish();
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
index b690f217..47efb83d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
@@ -25,6 +25,7 @@ import android.support.v7.app.AppCompatDelegate;
import com.evernote.android.job.JobManager;
import com.jakewharton.picasso.OkHttp3Downloader;
+import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.squareup.picasso.Picasso;
@@ -33,6 +34,7 @@ public class TuskyApplication extends Application {
public static final String APP_THEME_DEFAULT = "AppTheme:prefer:night";
private static AppDatabase db;
+ private static AccountManager accountManager;
public static AppDatabase getDB() {
return db;
@@ -71,5 +73,12 @@ public class TuskyApplication extends Application {
//necessary for Android < APi 21
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
+
+ accountManager = new AccountManager();
}
-}
\ No newline at end of file
+
+ public static AccountManager getAccountManager() {
+ return accountManager;
+ }
+
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.java b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt
similarity index 51%
rename from app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.java
rename to app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt
index b80cb5fa..a51b2e2b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt
@@ -1,4 +1,4 @@
-/* Copyright 2017 Andrew Dawson
+/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
@@ -13,19 +13,19 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.receiver;
+package com.keylesspalace.tusky.db
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
+import android.arch.persistence.room.*
+
+@Dao
+interface AccountDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertOrReplace(account: AccountEntity): Long
+
+ @Delete
+ fun delete(account: AccountEntity)
+
+ @Query("SELECT * FROM AccountEntity ORDER BY id ASC")
+ fun loadAll(): List
-public class NotificationClearBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- SharedPreferences notificationPreferences = context.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = notificationPreferences.edit();
- editor.putString("current", "[]");
- editor.apply();
- }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
new file mode 100644
index 00000000..ccf87360
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
@@ -0,0 +1,67 @@
+/* Copyright 2018 Conny Duck
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.db
+
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.Index
+import android.arch.persistence.room.PrimaryKey
+
+@Entity(indices = [Index(value = ["domain", "accountId"],
+ unique = true)])
+data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
+ val domain: String,
+ var accessToken: String,
+ var isActive: Boolean,
+ var accountId: String = "",
+ var username: String = "",
+ var displayName: String = "",
+ var profilePictureUrl: String = "",
+ var notificationsEnabled: Boolean = true,
+ var notificationsMentioned: Boolean = true,
+ var notificationsFollowed: Boolean = true,
+ var notificationsReblogged: Boolean = true,
+ var notificationsFavorited: Boolean = true,
+ var notificationSound: Boolean = true,
+ var notificationVibration: Boolean = true,
+ var notificationLight: Boolean = true,
+ var lastNotificationId: String = "0",
+ var activeNotifications: String = "[]") {
+
+ val identifier: String
+ get() = "$domain:$accountId"
+
+ val fullName: String
+ get() = "@$username@$domain"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AccountEntity
+
+ if (id == other.id) return true
+ if (domain == other.domain && accountId == other.accountId) return true
+
+ return false
+ }
+
+ override fun hashCode(): Int {
+ var result = id.hashCode()
+ result = 31 * result + domain.hashCode()
+ result = 31 * result + accountId.hashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
new file mode 100644
index 00000000..b28c0137
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
@@ -0,0 +1,190 @@
+/* Copyright 2018 Conny Duck
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.db
+
+import android.util.Log
+import com.keylesspalace.tusky.TuskyApplication
+import com.keylesspalace.tusky.entity.Account
+
+/**
+ * This class caches the account database and handles all account related operations
+ * @author ConnyDuck
+ */
+
+class AccountManager {
+
+ @Volatile var activeAccount: AccountEntity? = null
+
+ private var accounts: MutableList = mutableListOf()
+ private val accountDao: AccountDao = TuskyApplication.getDB().accountDao()
+
+ init {
+ accounts = accountDao.loadAll().toMutableList()
+
+ activeAccount = accounts.find { acc ->
+ acc.isActive
+ }
+ }
+
+ /**
+ * Adds a new empty account and makes it the active account.
+ * More account information has to be added later with [updateActiveAccount]
+ * or the account wont be saved to the database.
+ * @param accessToken the access token for the new account
+ * @param domain the domain of the accounts Mastodon instance
+ */
+ fun addAccount(accessToken: String, domain: String) {
+
+ activeAccount?.let{
+ it.isActive = false
+ Log.d("AccountManager", "saving account with id "+it.id)
+
+ accountDao.insertOrReplace(it)
+ }
+
+ activeAccount = AccountEntity(id = 0, domain = domain, accessToken = accessToken, isActive = true)
+
+ }
+
+ /**
+ * Saves an already known account to the database.
+ * New accounts must be created with [addAccount]
+ * @param account the account to save
+ */
+ fun saveAccount(account: AccountEntity) {
+ if(account.id != 0L) {
+ Log.d("AccountManager", "saving account with id "+account.id)
+ val index = accounts.indexOf(account)
+ if (index != -1) {
+ accounts.removeAt(index)
+ accounts.add(account)
+ }
+
+ accountDao.insertOrReplace(account)
+ }
+
+ }
+
+ /**
+ * Logs the current account out by deleting all data of the account.
+ * @return the new active account, or null if no other account was found
+ */
+ fun logActiveAccountOut() : AccountEntity? {
+
+ if(activeAccount == null) {
+ return null
+ } else {
+ accounts.remove(activeAccount!!)
+ accountDao.delete(activeAccount!!)
+
+ if(accounts.size > 0) {
+ accounts[0].isActive = true
+ activeAccount = accounts[0]
+ accountDao.insertOrReplace(accounts[0])
+ } else {
+ activeAccount = null
+ }
+ return activeAccount
+
+ }
+
+ }
+
+ /**
+ * updates the current account with new information from the mastodon api
+ * and saves it in the database
+ * @param account the [Account] object returned from the api
+ */
+ fun updateActiveAccount(account: Account) {
+ activeAccount?.let{
+ it.accountId = account.id
+ it.username = account.username
+ it.displayName = account.getDisplayName()
+ it.profilePictureUrl = account.avatar
+
+ Log.d("AccountManager", "id before save "+it.id)
+ it.id = accountDao.insertOrReplace(it)
+ Log.d("AccountManager", "id after save "+it.id)
+
+
+ val accountIndex = accounts.indexOf(it)
+
+ if(accountIndex != -1) {
+ //in case the user was already logged in with this account, remove the old information
+ accounts.removeAt(accountIndex)
+ accounts.add(accountIndex, it)
+ } else {
+ accounts.add(it)
+ }
+
+ }
+ }
+
+ /**
+ * changes the active account
+ * @param accountId the database id of the new active account
+ */
+ fun setActiveAccount(accountId: Long) {
+
+ activeAccount?.let{
+ it.isActive = false
+ accountDao.insertOrReplace(it)
+ }
+
+ activeAccount = accounts.find { acc ->
+ acc.id == accountId
+ }
+
+ activeAccount?.let{
+ it.isActive = true
+ accountDao.insertOrReplace(it)
+ }
+ }
+
+ /**
+ * @return an immutable list of all accounts in the database with the active account first
+ */
+ fun getAllAccountsOrderedByActive(): List {
+ accounts.sortWith (Comparator { l, r ->
+ when {
+ l.isActive && !r.isActive -> -1
+ r.isActive && !l.isActive -> 1
+ else -> 0
+ }
+ })
+
+ return accounts.toList()
+ }
+
+ /**
+ * @return true if at least one account has notifications enabled
+ */
+ fun notificationsEnabled(): Boolean {
+ return accounts.any { it.notificationsEnabled }
+ }
+
+ /**
+ * Finds an account by its database id
+ * @param accountId the id of the account
+ * @return the requested account or null if it was not found
+ */
+ fun getAccountById(accountId: Long): AccountEntity? {
+ return accounts.find { acc ->
+ acc.id == accountId
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
index 9d87b547..9fcc62a5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
@@ -25,10 +25,11 @@ import android.support.annotation.NonNull;
* DB version & declare DAO
*/
-@Database(entities = {TootEntity.class}, version = 4, exportSchema = false)
+@Database(entities = {TootEntity.class, AccountEntity.class}, version = 4, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
+ public abstract AccountDao accountDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
index 9eedf178..aecc5deb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
@@ -37,9 +37,12 @@ import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.MainActivity;
+import com.keylesspalace.tusky.TuskyApplication;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.db.AccountEntity;
+import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Status;
@@ -57,6 +60,7 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
+import java.math.BigInteger;
import java.util.Iterator;
import java.util.List;
@@ -79,7 +83,7 @@ public class NotificationsFragment extends SFragment implements
}
/**
- * Placeholder for the notifications. Consider moving to the separate class to hide constructor
+ * Placeholder for the notificationsEnabled. Consider moving to the separate class to hide constructor
* and reuse in different places as needed.
*/
private static final class Placeholder {
@@ -200,7 +204,7 @@ public class NotificationsFragment extends SFragment implements
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then.
- * Use a modified scroll listener that both loads more notifications as it goes, and hides
+ * Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides
* the compose button on down-scroll. */
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
preferences.registerOnSharedPreferenceChangeListener(this);
@@ -552,14 +556,13 @@ public class NotificationsFragment extends SFragment implements
}
update(notifications, fromId, uptoId);
}
- /* Set last update id for pull notifications so that we don't get notified
- * about things we already loaded here */
- getPrivatePreferences().edit()
- .putString("lastUpdateId", fromId)
- .apply();
+
break;
}
}
+
+ saveNewestNotificationId(notifications);
+
fulfillAnyQueuedFetches(fetchEnd);
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(FooterViewHolder.State.EMPTY);
@@ -581,6 +584,29 @@ public class NotificationsFragment extends SFragment implements
fulfillAnyQueuedFetches(fetchEnd);
}
+ private void saveNewestNotificationId(List notifications) {
+ AccountManager accountManager = TuskyApplication.getAccountManager();
+ AccountEntity account = accountManager.getActiveAccount();
+ BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
+
+ for (Notification noti: notifications) {
+ BigInteger a = new BigInteger(noti.id);
+ if(isBiggerThan(a, lastNoti)) {
+ lastNoti = a;
+ }
+ }
+
+ Log.d(TAG, "saving newest noti id: " + lastNoti);
+
+ account.setLastNotificationId(lastNoti.toString());
+ accountManager.saveAccount(account);
+ }
+
+ private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
+
+ return lastShownNotificationId.compareTo(newId) == - 1;
+ }
+
private void update(@Nullable List newNotifications, @Nullable String fromId,
@Nullable String uptoId) {
if (ListUtils.isEmpty(newNotifications)) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java
index 7fab0c39..7103c5e1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/PreferencesFragment.java
@@ -19,6 +19,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
+import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
@@ -27,7 +28,8 @@ import android.support.annotation.XmlRes;
import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.PreferencesActivity;
import com.keylesspalace.tusky.R;
-import com.keylesspalace.tusky.util.NotificationManager;
+import com.keylesspalace.tusky.TuskyApplication;
+import com.keylesspalace.tusky.db.AccountEntity;
public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
SharedPreferences sharedPreferences;
@@ -58,23 +60,28 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
if(notificationPreferences != null) {
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && activeAccount != null) {
+ notificationPreferences.setSummary(getString(R.string.pref_summary_notifications, activeAccount.getFullName()));
+ }
+
+
//on Android O and newer, launch the system notification settings instead of the app settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- NotificationManager.createNotificationChannels(getContext());
+ notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent();
+ intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
- notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
- @Override
- public boolean onPreferenceClick(Preference preference) {
- Intent intent = new Intent();
- intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
+ intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID);
- intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID);
-
- startActivity(intent);
- return true;
- }
- });
+ startActivity(intent);
+ return true;
+ }
+ });
} else {
notificationPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@@ -122,6 +129,38 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
});
}
+ if(preference == R.xml.notification_preferences) {
+
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
+
+ if(activeAccount != null) {
+
+ CheckBoxPreference notificationPref = (CheckBoxPreference) findPreference("notificationsEnabled");
+ notificationPref.setChecked(activeAccount.getNotificationsEnabled());
+
+ CheckBoxPreference mentionedPref = (CheckBoxPreference) findPreference("notificationFilterMentions");
+ mentionedPref.setChecked(activeAccount.getNotificationsMentioned());
+
+ CheckBoxPreference followedPref = (CheckBoxPreference) findPreference("notificationFilterFollows");
+ followedPref.setChecked(activeAccount.getNotificationsFollowed());
+
+ CheckBoxPreference boostedPref = (CheckBoxPreference) findPreference("notificationFilterReblogs");
+ boostedPref.setChecked(activeAccount.getNotificationsReblogged());
+
+ CheckBoxPreference favoritedPref = (CheckBoxPreference) findPreference("notificationFilterFavourites");
+ favoritedPref.setChecked(activeAccount.getNotificationsFavorited());
+
+ CheckBoxPreference soundPref = (CheckBoxPreference) findPreference("notificationAlertSound");
+ soundPref.setChecked(activeAccount.getNotificationSound());
+
+ CheckBoxPreference vibrationPref = (CheckBoxPreference) findPreference("notificationAlertVibrate");
+ vibrationPref.setChecked(activeAccount.getNotificationVibration());
+
+ CheckBoxPreference lightPref = (CheckBoxPreference) findPreference("notificationAlertLight");
+ lightPref.setChecked(activeAccount.getNotificationLight());
+ }
+ }
+
}
@Override
@@ -150,15 +189,50 @@ public class PreferencesFragment extends PreferenceFragment implements SharedPre
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
+
switch (key) {
case "httpProxyServer":
case "httpProxyPort":
updateSummary(key);
case "httpProxyEnabled":
httpProxyChanged = true;
- break;
+ return;
default:
}
+
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
+
+ if(activeAccount != null) {
+ switch(key) {
+ case "notificationsEnabled":
+ activeAccount.setNotificationsEnabled(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationFilterMentions":
+ activeAccount.setNotificationsMentioned(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationFilterFollows":
+ activeAccount.setNotificationsFollowed(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationFilterReblogs":
+ activeAccount.setNotificationsReblogged(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationFilterFavourites":
+ activeAccount.setNotificationsFavorited(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationAlertSound":
+ activeAccount.setNotificationSound(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationAlertVibrate":
+ activeAccount.setNotificationVibration(sharedPreferences.getBoolean(key, true));
+ break;
+ case "notificationAlertLight":
+ activeAccount.setNotificationLight(sharedPreferences.getBoolean(key, true));
+ break;
+ }
+ TuskyApplication.getAccountManager().saveAccount(activeAccount);
+
+ }
+
}
private void updateSummary(String key) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index 7a9a96ec..4a6d73dd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -19,7 +19,6 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -36,10 +35,12 @@ import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.ComposeActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ReportActivity;
+import com.keylesspalace.tusky.TuskyApplication;
import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.ViewVideoActivity;
+import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
@@ -73,9 +74,11 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- SharedPreferences preferences = getPrivatePreferences();
- loggedInAccountId = preferences.getString("loggedInAccountId", null);
- loggedInUsername = preferences.getString("loggedInAccountUsername", null);
+ AccountEntity activeAccount = TuskyApplication.getAccountManager().getActiveAccount();
+ if(activeAccount != null) {
+ loggedInAccountId = activeAccount.getAccountId();
+ loggedInUsername = activeAccount.getUsername();
+ }
}
@Override
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/AuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/AuthInterceptor.java
index 1b615027..60d0dd31 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/AuthInterceptor.java
+++ b/app/src/main/java/com/keylesspalace/tusky/network/AuthInterceptor.java
@@ -1,11 +1,9 @@
package com.keylesspalace.tusky.network;
-import android.content.Context;
-import android.content.SharedPreferences;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.TuskyApplication;
+import com.keylesspalace.tusky.db.AccountEntity;
import java.io.IOException;
@@ -17,37 +15,24 @@ import okhttp3.Response;
* Created by charlag on 31/10/17.
*/
-public final class AuthInterceptor implements Interceptor, SharedPreferences.OnSharedPreferenceChangeListener {
+public final class AuthInterceptor implements Interceptor {
- private static final String TOKEN_KEY = "accessToken";
-
- @Nullable
- private String token;
-
- public AuthInterceptor(Context context) {
- SharedPreferences preferences = context.getSharedPreferences(
- context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
- token = preferences.getString(TOKEN_KEY, null);
- preferences.registerOnSharedPreferenceChangeListener(this);
- }
+ public AuthInterceptor() { }
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
+
+ AccountEntity currentAccount = TuskyApplication.getAccountManager().getActiveAccount();
+
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
- if (token != null) {
- builder.header("Authorization", String.format("Bearer %s", token));
+ if (currentAccount != null) {
+ builder.header("Authorization", String.format("Bearer %s", currentAccount.getAccessToken()));
}
Request newRequest = builder.build();
return chain.proceed(newRequest);
}
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (key.equals(TOKEN_KEY)) {
- token = sharedPreferences.getString(TOKEN_KEY, null);
- }
- }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
index 06576b08..37cc8a7c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
@@ -40,6 +40,7 @@ import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
+import retrofit2.http.Header;
import retrofit2.http.Multipart;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
@@ -81,6 +82,9 @@ public interface MastodonApi {
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
+ @GET("api/v1/notifications")
+ Call> notificationsWithAuth(
+ @Header("Authorization") String auth);
@POST("api/v1/notifications/clear")
Call clearNotifications();
@GET("api/v1/notifications/{id}")
diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt
new file mode 100644
index 00000000..be8f84b2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt
@@ -0,0 +1,38 @@
+/* Copyright 2018 Conny Duck
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+import com.keylesspalace.tusky.TuskyApplication
+import com.keylesspalace.tusky.util.NotificationManager
+
+class NotificationClearBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+
+ val accountId = intent.getLongExtra(NotificationManager.ACCOUNT_ID, -1)
+
+ val accountManager = TuskyApplication.getAccountManager()
+ val account = accountManager.getAccountById(accountId)
+ if (account != null) {
+ account.activeNotifications = "[]"
+ accountManager.saveAccount(account)
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationManager.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationManager.java
index d42119d1..05dc5fd3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationManager.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationManager.java
@@ -16,14 +16,13 @@
package com.keylesspalace.tusky.util;
import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
-import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
@@ -32,8 +31,10 @@ import android.support.v4.content.ContextCompat;
import android.util.Log;
import com.keylesspalace.tusky.MainActivity;
-import com.keylesspalace.tusky.NotificationPullJobCreator;
import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.TuskyApplication;
+import com.keylesspalace.tusky.db.AccountEntity;
+import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
import com.keylesspalace.tusky.view.RoundedTransformation;
@@ -47,11 +48,13 @@ import java.util.ArrayList;
import java.util.List;
public class NotificationManager {
+
+ /** constants used in Intents */
+ public static final String ACCOUNT_ID = "account_id";
+
private static final String TAG = "NotificationManager";
- /**
- * notification channels used on Android O+
- **/
+ /** notification channels used on Android O+ **/
private static final String CHANNEL_MENTION = "CHANNEL_MENTION";
private static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW";
private static final String CHANNEL_BOOST = "CHANNEL_BOOST";
@@ -62,22 +65,17 @@ public class NotificationManager {
* the state of the existing notification to reflect the new interaction.
*
* @param context to access application preferences and services
- * @param notifyId an arbitrary number to reference this notification for any future action
* @param body a new Mastodon notification
+ * @param account the account for which the notification should be shown
*/
- public static void make(final Context context, final int notifyId, Notification body) {
- final SharedPreferences preferences =
- PreferenceManager.getDefaultSharedPreferences(context);
- final SharedPreferences notificationPreferences = context.getSharedPreferences(
- "Notifications", Context.MODE_PRIVATE);
- if (!filterNotification(preferences, body)) {
+ public static void make(final Context context, Notification body, AccountEntity account) {
+
+ if (!filterNotification(account, body)) {
return;
}
- createNotificationChannels(context);
-
- String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
+ String rawCurrentNotifications = account.getActiveNotifications();
JSONArray currentNotifications;
try {
@@ -102,34 +100,40 @@ public class NotificationManager {
currentNotifications.put(body.account.getDisplayName());
}
- notificationPreferences.edit()
- .putString("current", currentNotifications.toString())
- .apply();
+ account.setActiveNotifications(currentNotifications.toString());
+
+ //no need to save account, this will be done in the calling function
Intent resultIntent = new Intent(context, MainActivity.class);
- resultIntent.putExtra("tab_position", 1);
+ resultIntent.putExtra(ACCOUNT_ID, account.getId());
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
- PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent((int)account.getId(),
PendingIntent.FLAG_UPDATE_CURRENT);
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
- PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
+ deleteIntent.putExtra(ACCOUNT_ID, account.getId());
+ PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, (int)account.getId(), deleteIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(body))
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
.setSmallIcon(R.drawable.ic_notify)
.setContentIntent(resultPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setColor(ContextCompat.getColor(context, (R.color.primary)))
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
- setupPreferences(preferences, builder);
+ setupPreferences(account, builder);
if (currentNotifications.length() == 1) {
builder.setContentTitle(titleForType(context, body))
- .setContentText(truncateWithEllipses(bodyForType(body), 40));
+ .setContentText(bodyForType(body));
+
+ if(body.type == Notification.Type.MENTION) {
+ builder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(bodyForType(body)));
+ }
//load the avatar synchronously
Bitmap accountAvatar;
@@ -149,7 +153,7 @@ public class NotificationManager {
try {
String format = context.getString(R.string.notification_title_summary);
String title = String.format(format, currentNotifications.length());
- String text = truncateWithEllipses(joinNames(context, currentNotifications), 40);
+ String text = joinNames(context, currentNotifications);
builder.setContentTitle(title)
.setContentText(text);
} catch (JSONException e) {
@@ -157,24 +161,29 @@ public class NotificationManager {
}
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
- builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
- }
+ builder.setSubText(account.getFullName());
+
+ builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
+ builder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
android.app.NotificationManager notificationManager = (android.app.NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
+
//noinspection ConstantConditions
- notificationManager.notify(notifyId, builder.build());
+ notificationManager.notify((int)account.getId(), builder.build());
}
- public static void createNotificationChannels(Context context) {
+ public static void createNotificationChannelsForAccount(AccountEntity account, Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
android.app.NotificationManager mNotificationManager =
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- String[] channelIds = new String[]{CHANNEL_MENTION, CHANNEL_FOLLOW, CHANNEL_BOOST, CHANNEL_FAVOURITE};
+ String[] channelIds = new String[]{
+ CHANNEL_MENTION+account.getIdentifier(),
+ CHANNEL_FOLLOW+account.getIdentifier(),
+ CHANNEL_BOOST+account.getIdentifier(),
+ CHANNEL_FAVOURITE+account.getIdentifier()};
int[] channelNames = {
R.string.notification_channel_mention_name,
R.string.notification_channel_follow_name,
@@ -190,6 +199,11 @@ public class NotificationManager {
List channels = new ArrayList<>(4);
+ NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
+
+ //noinspection ConstantConditions
+ mNotificationManager.createNotificationChannelGroup(channelGroup);
+
for (int i = 0; i < channelIds.length; i++) {
String id = channelIds[i];
String name = context.getString(channelNames[i]);
@@ -201,6 +215,7 @@ public class NotificationManager {
channel.enableLights(true);
channel.enableVibration(true);
channel.setShowBadge(true);
+ channel.setGroup(account.getIdentifier());
channels.add(channel);
}
@@ -210,20 +225,48 @@ public class NotificationManager {
}
}
- public static void clearNotifications(@Nullable Context context) {
- if(context != null) {
- SharedPreferences notificationPreferences =
- context.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
- notificationPreferences.edit().putString("current", "[]").apply();
+ public static void deleteNotificationChannelsForAccount(AccountEntity account, Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+
+ android.app.NotificationManager mNotificationManager =
+ (android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ //noinspection ConstantConditions
+ mNotificationManager.deleteNotificationChannelGroup(account.getIdentifier());
+
+ }
+ }
+
+ public static void deleteLegacyNotificationChannels(Context context) {
+ // delete the notification channels that where used before the multi account mode was introduced to avoid confusion
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+
+ android.app.NotificationManager mNotificationManager =
+ (android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ //noinspection ConstantConditions
+ mNotificationManager.deleteNotificationChannel(CHANNEL_MENTION);
+ mNotificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
+ mNotificationManager.deleteNotificationChannel(CHANNEL_BOOST);
+ mNotificationManager.deleteNotificationChannel(CHANNEL_FOLLOW);
+ }
+ }
+
+ public static void clearNotificationsForActiveAccount(Context context) {
+ AccountManager accountManager = TuskyApplication.getAccountManager();
+ AccountEntity account = accountManager.getActiveAccount();
+ if (account != null) {
+ account.setActiveNotifications("[]");
+ accountManager.saveAccount(account);
android.app.NotificationManager manager = (android.app.NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
//noinspection ConstantConditions
- manager.cancel(NotificationPullJobCreator.NOTIFY_ID);
+ manager.cancel((int)account.getId());
}
}
- private static boolean filterNotification(SharedPreferences preferences,
+ private static boolean filterNotification(AccountEntity account,
Notification notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -233,56 +276,47 @@ public class NotificationManager {
switch (notification.type) {
default:
case MENTION:
- return preferences.getBoolean("notificationFilterMentions", true);
+ return account.getNotificationsMentioned();
case FOLLOW:
- return preferences.getBoolean("notificationFilterFollows", true);
+ return account.getNotificationsFollowed();
case REBLOG:
- return preferences.getBoolean("notificationFilterReblogs", true);
+ return account.getNotificationsReblogged();
case FAVOURITE:
- return preferences.getBoolean("notificationFilterFavourites", true);
+ return account.getNotificationsFavorited();
}
}
- private static String getChannelId(Notification notification) {
+ private static String getChannelId(AccountEntity account, Notification notification) {
switch (notification.type) {
default:
case MENTION:
- return CHANNEL_MENTION;
+ return CHANNEL_MENTION+account.getIdentifier();
case FOLLOW:
- return CHANNEL_FOLLOW;
+ return CHANNEL_FOLLOW+account.getIdentifier();
case REBLOG:
- return CHANNEL_BOOST;
+ return CHANNEL_BOOST+account.getIdentifier();
case FAVOURITE:
- return CHANNEL_FAVOURITE;
+ return CHANNEL_FAVOURITE+account.getIdentifier();
}
}
- @SuppressWarnings("SameParameterValue")
- private static String truncateWithEllipses(String string, int limit) {
- if (string.length() < limit) {
- return string;
- } else {
- return string.substring(0, limit - 3) + "...";
- }
- }
-
- private static void setupPreferences(SharedPreferences preferences,
+ private static void setupPreferences(AccountEntity account,
NotificationCompat.Builder builder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return; //do nothing on Android O or newer, the system uses the channel settings anyway
}
- if (preferences.getBoolean("notificationAlertSound", true)) {
+ if (account.getNotificationSound()) {
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
}
- if (preferences.getBoolean("notificationAlertVibrate", false)) {
+ if (account.getNotificationVibration()) {
builder.setVibrate(new long[]{500, 500});
}
- if (preferences.getBoolean("notificationAlertLight", false)) {
+ if (account.getNotificationLight()) {
builder.setLights(0xFF00FF8F, 300, 1000);
}
}
@@ -326,7 +360,7 @@ public class NotificationManager {
private static String bodyForType(Notification notification) {
switch (notification.type) {
case FOLLOW:
- return notification.account.username;
+ return "@"+notification.account.username;
case MENTION:
case FAVOURITE:
case REBLOG:
diff --git a/app/src/main/res/anim/explode.xml b/app/src/main/res/anim/explode.xml
new file mode 100644
index 00000000..27982abc
--- /dev/null
+++ b/app/src/main/res/anim/explode.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-xxhdpi/ic_reblog_dark_.png b/app/src/main/res/drawable-xxhdpi/ic_reblog_dark_.png
new file mode 100644
index 00000000..cf90f688
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_reblog_dark_.png differ
diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml
index e485a29e..5167d83c 100644
--- a/app/src/main/res/layout/activity_compose.xml
+++ b/app/src/main/res/layout/activity_compose.xml
@@ -16,7 +16,16 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginBottom="8dp"
- android:background="@android:color/transparent" />
+ android:background="@android:color/transparent">
+
+
+
-
-
-
-
+ android:layout_height="match_parent"
+ android:fillViewport="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_follow.xml b/app/src/main/res/layout/item_follow.xml
index 1f7707cf..2525c98b 100644
--- a/app/src/main/res/layout/item_follow.xml
+++ b/app/src/main/res/layout/item_follow.xml
@@ -1,5 +1,5 @@
+
حرر
التسجيل بواسطة ماستدون
خروج
- هل تود تسجيل الخروج ؟
إتبع
إلغاء التتبع
قم بحظره
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 276040a7..a5ebbc49 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -60,7 +60,6 @@
Redacta
Inicia sessió amb Mastodon
Tanca sessió
- Vols tancar la sessió?
Segueix
Deixa de seguir
Bloca
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 5252435e..5d34e7c6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -216,7 +216,6 @@
Zeige Boosts
Überprüfungsintervall
leer
- Willst du dich wirklich ausloggen?
Verstecke Medien
Timeline-Filter
Gespeicherte Tröts
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index f97a28d7..dc1fa9d4 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -93,7 +93,6 @@
Sauvegarder
Modifier le profil
Accepter
- Voulez-vous vous déconnectez ?
Rejeter
Annuler
Demandes de suivi
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index c1e09296..e2dee433 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -57,7 +57,6 @@
Szerkeszt
Bejelentkezés Mastodon-al
Kijelentkezés
- Ki szeretne jelentkezni?
Követ
Követőktől eltávolít
Blokkol
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 6944237c..07ca80e5 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -57,7 +57,7 @@
新規投稿
Mastodonでログイン
ログアウト
- ログアウトしますか?
+
フォローする
フォロー解除
ブロック
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index d920d65d..ae8299f3 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -51,7 +51,6 @@
Schrijven
Inloggen bij Mastodon
Uitloggen
- Wil je echt uitloggen?
Volgen
Ontvolgen
Blokkeren
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8dc90e95..f1395ccf 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -68,9 +68,10 @@
Napisz
Zaloguj z Mastodon
Wyloguj
- Czy chcesz wylogować się?
+
Śledź
Przestań śledzić
+
Zablokuj
Odblokuj
Zgłoś
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 9908e77e..0e61b3f8 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -60,7 +60,6 @@
Compor
Entrar com Mastodon
Sair
- Deseja sair?
Seguir
Deixar de seguir
Bloquear
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 2a1d6c14..7dd904b0 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -61,7 +61,6 @@
Написать
Войти
Выйти
- Вы хотите выйти?
Подписаться
Отписаться
Заблокировать
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 5a18fb9b..58979c0a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -4,6 +4,8 @@
#000000
#8f000000
#33000000
+ #44000000
+
#4c5368
#313543
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 16a7dc04..a6409922 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,7 +61,7 @@
Compose
Login with Mastodon
Log Out
- Do you wish to logout
+ Are you sure you want to log out of the account %1$s?
Follow
Unfollow
Block
@@ -157,6 +157,7 @@
Notifications
Edit Notifications
Notifications
+ for account %1$s
Check Interval
Alerts
Notify with a sound
@@ -230,7 +231,7 @@
Boosts
Notifications when your toots get boosted
Favourites
- Notifications when your toots get mark as favourite
+ Notifications when your toots get marked as favourite
%s mentioned you
@@ -285,9 +286,16 @@
Media
Replying to @%s
load more
+
+ Add Account
+ Add new Mastodon Account
+
Lists
Lists
List timeline
+
+ Posting with account %1$s
+
Failed to set caption
Describe for visually impaired
Set caption