diff --git a/build.gradle b/build.gradle
index c61b945e..c4715843 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath "com.android.tools.build:gradle:7.1.3"
+ classpath "com.android.tools.build:gradle:7.4.2"
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 464f9805..e9cd6f21 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Jan 13 11:33:43 MSK 2022
+#Sat Jun 03 23:40:27 MSK 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/mastodon/build.gradle b/mastodon/build.gradle
index 55bab126..124bd826 100644
--- a/mastodon/build.gradle
+++ b/mastodon/build.gradle
@@ -9,7 +9,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
- versionCode 55
+ versionCode 56
versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
@@ -37,6 +37,9 @@ android {
githubRelease{
initWith release
}
+ githubDebug{
+ initWith debug
+ }
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -53,6 +56,9 @@ android {
githubRelease{
setRoot "src/github"
}
+ githubDebug{
+ setRoot "src/github"
+ }
}
lintOptions{
checkReleaseBuilds false
@@ -77,7 +83,7 @@ dependencies {
implementation 'de.psdev:async-otto:1.0.3'
implementation 'org.parceler:parceler-api:1.1.12'
annotationProcessor 'org.parceler:parceler:1.1.12'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
def appCenterSdkVersion = "4.4.2"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
diff --git a/mastodon/src/github/java/org/joinmastodon/android/updater/GithubSelfUpdaterImpl.java b/mastodon/src/github/java/org/joinmastodon/android/updater/GithubSelfUpdaterImpl.java
index 314fee66..b607c8e4 100644
--- a/mastodon/src/github/java/org/joinmastodon/android/updater/GithubSelfUpdaterImpl.java
+++ b/mastodon/src/github/java/org/joinmastodon/android/updater/GithubSelfUpdaterImpl.java
@@ -95,7 +95,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
return;
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
- if(timeSinceLastCheck>CHECK_PERIOD){
+ if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
@@ -109,23 +109,26 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
try(Response resp=call.execute()){
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
String tag=obj.get("tag_name").getAsString();
- Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)");
+ Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)(?:\\.(\\d+))?");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
- int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
- matcher=pattern.matcher(BuildConfig.VERSION_NAME);
- if(!matcher.find()){
+ int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=matcher.group(3)!=null ? Integer.parseInt(matcher.group(3)) : 0;
+ Matcher curMatcher=pattern.matcher(BuildConfig.VERSION_NAME);
+ if(!curMatcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
- int curMajor=Integer.parseInt(matcher.group(1)), curMinor=Integer.parseInt(matcher.group(2)), curRevision=Integer.parseInt(matcher.group(3));
+ int curMajor=Integer.parseInt(curMatcher.group(1)), curMinor=Integer.parseInt(curMatcher.group(2)), curRevision=matcher.group(3)!=null ? Integer.parseInt(curMatcher.group(3)) : 0;
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
- if(newVersion>curVersion || BuildConfig.DEBUG){
- String version=newMajor+"."+newMinor+"."+newRevision;
+ if(newVersion>curVersion || forceUpdate){
+ forceUpdate=false;
+ String version=newMajor+"."+newMinor;
+ if(matcher.group(3)!=null)
+ version+="."+newRevision;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
@@ -295,6 +298,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
}
}
+ @Override
+ public void reset(){
+ getPrefs().edit().clear().apply();
+ File apk=getUpdateApkFile();
+ if(apk.exists())
+ apk.delete();
+ state=UpdateState.NO_UPDATE;
+ }
+
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
@Override
diff --git a/mastodon/src/main/assets/server_about_template.htm b/mastodon/src/main/assets/server_about_template.htm
new file mode 100644
index 00000000..c3cec86e
--- /dev/null
+++ b/mastodon/src/main/assets/server_about_template.htm
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+{{content}}
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
index 5ffa48d5..5f7eaf8d 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java
@@ -6,7 +6,7 @@ import android.content.SharedPreferences;
public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
- public static boolean trueBlackTheme;
+ public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme;
private static SharedPreferences getPrefs(){
@@ -17,7 +17,10 @@ public class GlobalUserPreferences{
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
- trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
+ altTextReminders=prefs.getBoolean("altTextReminders", false);
+ confirmUnfollow=prefs.getBoolean("confirmUnfollow", false);
+ confirmBoost=prefs.getBoolean("confirmBoost", false);
+ confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
}
@@ -25,8 +28,11 @@ public class GlobalUserPreferences{
getPrefs().edit()
.putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs)
- .putBoolean("trueBlackTheme", trueBlackTheme)
.putInt("theme", theme.ordinal())
+ .putBoolean("altTextReminders", altTextReminders)
+ .putBoolean("confirmUnfollow", confirmUnfollow)
+ .putBoolean("confirmBoost", confirmBoost)
+ .putBoolean("confirmDeletePost", confirmDeletePost)
.apply();
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java b/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java
index 84333cbe..61cd8dee 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java
@@ -3,11 +3,10 @@ package org.joinmastodon.android;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
+import android.webkit.WebView;
import org.joinmastodon.android.api.PushSubscriptionManager;
-import java.lang.reflect.InvocationTargetException;
-
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.NetworkUtils;
import me.grishka.appkit.utils.V;
@@ -30,5 +29,8 @@ public class MastodonApp extends Application{
PushSubscriptionManager.tryRegisterFCM();
GlobalUserPreferences.load();
+ if(BuildConfig.DEBUG){
+ WebView.setWebContentsDebuggingEnabled(true);
+ }
}
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
index 16c45495..cf5123fb 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
@@ -69,6 +69,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
return;
}
+ if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
+ Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
+ return;
+ }
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java
index ee56c5a3..eb34a4e4 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java
@@ -15,7 +15,8 @@ import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
@@ -59,7 +60,7 @@ public class CacheController{
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
- List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
+ List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
@@ -74,7 +75,7 @@ public class CacheController{
int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id;
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(status))
continue outer;
}
@@ -139,7 +140,7 @@ public class CacheController{
}
return;
}
- List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
+ List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
@@ -153,7 +154,7 @@ public class CacheController{
ntf.postprocess();
newMaxID=ntf.id;
if(ntf.status!=null){
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(ntf.status))
continue outer;
}
@@ -176,7 +177,7 @@ public class CacheController{
public void onSuccess(List result){
PaginatedResponse> res=new PaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(ntf.status)){
return false;
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java
index 5610155b..36071d05 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java
@@ -122,13 +122,17 @@ public class MastodonAPIController{
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
- else
+ else if(req.respClass!=null)
respObj=gson.fromJson(respJson, req.respClass);
+ else
+ respObj=null;
}else{
if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType());
- else
+ else if(req.respClass!=null)
respObj=gson.fromJson(reader, req.respClass);
+ else
+ respObj=null;
}
}catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG)
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/ResultlessMastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/ResultlessMastodonAPIRequest.java
new file mode 100644
index 00000000..26900f06
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/ResultlessMastodonAPIRequest.java
@@ -0,0 +1,9 @@
+package org.joinmastodon.android.api;
+
+import com.google.gson.reflect.TypeToken;
+
+public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest{
+ public ResultlessMastodonAPIRequest(HttpMethod method, String path){
+ super(method, path, (Class)null);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java
new file mode 100644
index 00000000..686b64e3
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java
@@ -0,0 +1,34 @@
+package org.joinmastodon.android.api.requests.accounts;
+
+import org.joinmastodon.android.api.MastodonAPIRequest;
+import org.joinmastodon.android.model.Account;
+import org.joinmastodon.android.model.Preferences;
+import org.joinmastodon.android.model.StatusPrivacy;
+
+public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest{
+ public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
+ super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
+ setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
+ }
+
+ private static class Request{
+ public Boolean locked, discoverable;
+ public RequestSource source;
+
+ public Request(Boolean locked, Boolean discoverable, RequestSource source){
+ this.locked=locked;
+ this.discoverable=discoverable;
+ this.source=source;
+ }
+ }
+
+ private static class RequestSource{
+ public StatusPrivacy privacy;
+ public String language;
+
+ public RequestSource(StatusPrivacy privacy, String language){
+ this.privacy=privacy;
+ this.language=language;
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/CreateFilter.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/CreateFilter.java
new file mode 100644
index 00000000..ac0477a1
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/CreateFilter.java
@@ -0,0 +1,23 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import org.joinmastodon.android.api.MastodonAPIRequest;
+import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterAction;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.FilterKeyword;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CreateFilter extends MastodonAPIRequest{
+ public CreateFilter(String title, EnumSet context, FilterAction action, int expiresIn, List words){
+ super(HttpMethod.POST, "/filters", Filter.class);
+ setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
+ }
+
+ @Override
+ protected String getPathPrefix(){
+ return "/api/v2";
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/DeleteFilter.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/DeleteFilter.java
new file mode 100644
index 00000000..6c5400a8
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/DeleteFilter.java
@@ -0,0 +1,14 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
+
+public class DeleteFilter extends ResultlessMastodonAPIRequest{
+ public DeleteFilter(String id){
+ super(HttpMethod.DELETE, "/filters/"+id);
+ }
+
+ @Override
+ protected String getPathPrefix(){
+ return "/api/v2";
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/FilterRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/FilterRequest.java
new file mode 100644
index 00000000..ff61d536
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/FilterRequest.java
@@ -0,0 +1,23 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import org.joinmastodon.android.model.FilterAction;
+import org.joinmastodon.android.model.FilterContext;
+
+import java.util.EnumSet;
+import java.util.List;
+
+class FilterRequest{
+ public String title;
+ public EnumSet context;
+ public FilterAction filterAction;
+ public Integer expiresIn;
+ public List keywordsAttributes;
+
+ public FilterRequest(String title, EnumSet context, FilterAction filterAction, Integer expiresIn, List keywordsAttributes){
+ this.title=title;
+ this.context=context;
+ this.filterAction=filterAction;
+ this.expiresIn=expiresIn;
+ this.keywordsAttributes=keywordsAttributes;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetFilters.java
similarity index 52%
rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java
rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetFilters.java
index 78103595..904d42f9 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetFilters.java
@@ -1,4 +1,4 @@
-package org.joinmastodon.android.api.requests.accounts;
+package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
import java.util.List;
-public class GetWordFilters extends MastodonAPIRequest>{
- public GetWordFilters(){
+public class GetFilters extends MastodonAPIRequest>{
+ public GetFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
+
+ @Override
+ protected String getPathPrefix(){
+ return "/api/v2";
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetLegacyFilters.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetLegacyFilters.java
new file mode 100644
index 00000000..eeefe13a
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/GetLegacyFilters.java
@@ -0,0 +1,14 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import com.google.gson.reflect.TypeToken;
+
+import org.joinmastodon.android.api.MastodonAPIRequest;
+import org.joinmastodon.android.model.LegacyFilter;
+
+import java.util.List;
+
+public class GetLegacyFilters extends MastodonAPIRequest>{
+ public GetLegacyFilters(){
+ super(HttpMethod.GET, "/filters", new TypeToken<>(){});
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/KeywordAttribute.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/KeywordAttribute.java
new file mode 100644
index 00000000..d35a0f0f
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/KeywordAttribute.java
@@ -0,0 +1,18 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import com.google.gson.annotations.SerializedName;
+
+class KeywordAttribute{
+ public String id;
+ @SerializedName("_destroy")
+ public Boolean delete;
+ public String keyword;
+ public Boolean wholeWord;
+
+ public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
+ this.id=id;
+ this.delete=delete;
+ this.keyword=keyword;
+ this.wholeWord=wholeWord;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/UpdateFilter.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/UpdateFilter.java
new file mode 100644
index 00000000..2c296540
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/filters/UpdateFilter.java
@@ -0,0 +1,30 @@
+package org.joinmastodon.android.api.requests.filters;
+
+import org.joinmastodon.android.api.MastodonAPIRequest;
+import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterAction;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.FilterKeyword;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class UpdateFilter extends MastodonAPIRequest{
+ public UpdateFilter(String id, String title, EnumSet context, FilterAction action, int expiresIn, List words, List deletedWords){
+ super(HttpMethod.PUT, "/filters/"+id, Filter.class);
+
+ List attrs=Stream.of(
+ words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
+ deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
+ ).flatMap(Function.identity()).collect(Collectors.toList());
+ setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
+ }
+
+ @Override
+ protected String getPathPrefix(){
+ return "/api/v2";
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstanceExtendedDescription.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstanceExtendedDescription.java
new file mode 100644
index 00000000..3d0487e8
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstanceExtendedDescription.java
@@ -0,0 +1,16 @@
+package org.joinmastodon.android.api.requests.instance;
+
+import org.joinmastodon.android.api.MastodonAPIRequest;
+
+import java.time.Instant;
+
+public class GetInstanceExtendedDescription extends MastodonAPIRequest{
+ public GetInstanceExtendedDescription(){
+ super(HttpMethod.GET, "/instance/extended_description", Response.class);
+ }
+
+ public static class Response{
+ public Instant updatedAt;
+ public String content;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java
new file mode 100644
index 00000000..92b5a26b
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java
@@ -0,0 +1,37 @@
+package org.joinmastodon.android.api.session;
+
+import android.content.SharedPreferences;
+
+public class AccountLocalPreferences{
+ private final SharedPreferences prefs;
+
+ public boolean showInteractionCounts;
+ public boolean customEmojiInNames;
+ public boolean showCWs;
+ public boolean hideSensitiveMedia;
+
+ public AccountLocalPreferences(SharedPreferences prefs){
+ this.prefs=prefs;
+ showInteractionCounts=prefs.getBoolean("interactionCounts", true);
+ customEmojiInNames=prefs.getBoolean("emojiInNames", true);
+ showCWs=prefs.getBoolean("showCWs", true);
+ hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
+ }
+
+ public long getNotificationsPauseEndTime(){
+ return prefs.getLong("notificationsPauseTime", 0L);
+ }
+
+ public void setNotificationsPauseEndTime(long time){
+ prefs.edit().putLong("notificationsPauseTime", time).apply();
+ }
+
+ public void save(){
+ prefs.edit()
+ .putBoolean("interactionCounts", showInteractionCounts)
+ .putBoolean("emojiInNames", customEmojiInNames)
+ .putBoolean("showCWs", showCWs)
+ .putBoolean("hideSensitive", hideSensitiveMedia)
+ .apply();
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java
index 4c4cc03f..158070da 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java
@@ -1,5 +1,6 @@
package org.joinmastodon.android.api.session;
+import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
@@ -7,17 +8,20 @@ import android.util.Log;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
+import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
+import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
+import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.TimelineMarkers;
@@ -46,7 +50,7 @@ public class AccountSession{
public PushSubscription pushSubscription;
public boolean needUpdatePushSettings;
public long filtersLastUpdated;
- public List wordFilters=new ArrayList<>();
+ public List wordFilters=new ArrayList<>();
public String pushAccountID;
public AccountActivationInfo activationInfo;
public Preferences preferences;
@@ -55,6 +59,8 @@ public class AccountSession{
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
private transient SharedPreferences prefs;
+ private transient boolean preferencesNeedSaving;
+ private transient AccountLocalPreferences localPreferences;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
@@ -106,7 +112,8 @@ public class AccountSession{
@Override
public void onSuccess(Preferences result){
preferences=result;
- callback.accept(result);
+ if(callback!=null)
+ callback.accept(result);
AccountSessionManager.getInstance().writeAccountsFile();
}
@@ -118,7 +125,7 @@ public class AccountSession{
.exec(getID());
}
- public SharedPreferences getLocalPreferences(){
+ public SharedPreferences getRawLocalPreferences(){
if(prefs==null)
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
return prefs;
@@ -150,11 +157,60 @@ public class AccountSession{
}
public String getLastKnownNotificationsMarker(){
- return getLocalPreferences().getString("notificationsMarker", null);
+ return getRawLocalPreferences().getString("notificationsMarker", null);
}
public void setNotificationsMarker(String id, boolean clearUnread){
- getLocalPreferences().edit().putString("notificationsMarker", id).apply();
+ getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
}
+
+ public void logOut(Activity activity, Runnable onDone){
+ new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Object result){
+ AccountSessionManager.getInstance().removeAccount(getID());
+ onDone.run();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ AccountSessionManager.getInstance().removeAccount(getID());
+ onDone.run();
+ }
+ })
+ .wrapProgress(activity, R.string.loading, false)
+ .exec(getID());
+ }
+
+ public void savePreferencesLater(){
+ preferencesNeedSaving=true;
+ }
+
+ public void savePreferencesIfPending(){
+ if(preferencesNeedSaving){
+ new UpdateAccountCredentialsPreferences(preferences, null, null)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Account result){
+ preferencesNeedSaving=false;
+ self=result;
+ AccountSessionManager.getInstance().writeAccountsFile();
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ Log.e(TAG, "failed to save preferences: "+error);
+ }
+ })
+ .exec(getID());
+ }
+ }
+
+ public AccountLocalPreferences getLocalPreferences(){
+ if(localPreferences==null)
+ localPreferences=new AccountLocalPreferences(getRawLocalPreferences());
+ return localPreferences;
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java
index 7b553d01..3015a63b 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java
@@ -13,8 +13,6 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
-import com.google.gson.JsonParseException;
-
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -22,7 +20,7 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
-import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
+import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance;
@@ -32,7 +30,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
@@ -190,6 +188,7 @@ public class AccountSessionManager{
lastActiveAccountID=null;
else
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
+ prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
writeAccountsFile();
String domain=session.domain.toLowerCase();
@@ -299,10 +298,10 @@ public class AccountSessionManager{
}
private void updateSessionWordFilters(AccountSession session){
- new GetWordFilters()
+ new GetLegacyFilters()
.setCallback(new Callback<>(){
@Override
- public void onSuccess(List result){
+ public void onSuccess(List result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile();
diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterCreatedOrUpdatedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterCreatedOrUpdatedEvent.java
new file mode 100644
index 00000000..2b85d964
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterCreatedOrUpdatedEvent.java
@@ -0,0 +1,13 @@
+package org.joinmastodon.android.events;
+
+import org.joinmastodon.android.model.Filter;
+
+public class SettingsFilterCreatedOrUpdatedEvent{
+ public final String accountID;
+ public final Filter filter;
+
+ public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
+ this.accountID=accountID;
+ this.filter=filter;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterDeletedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterDeletedEvent.java
new file mode 100644
index 00000000..d89069ed
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/events/SettingsFilterDeletedEvent.java
@@ -0,0 +1,11 @@
+package org.joinmastodon.android.events;
+
+public class SettingsFilterDeletedEvent{
+ public final String accountID;
+ public final String filterID;
+
+ public SettingsFilterDeletedEvent(String accountID, String filterID){
+ this.accountID=accountID;
+ this.filterID=filterID;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java
new file mode 100644
index 00000000..b4f63a09
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java
@@ -0,0 +1,9 @@
+package org.joinmastodon.android.events;
+
+public class StatusDisplaySettingsChangedEvent{
+ public final String accountID;
+
+ public StatusDisplaySettingsChangedEvent(String accountID){
+ this.accountID=accountID;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
index f1997c0b..6c229b2c 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
@@ -554,6 +554,14 @@ public abstract class BaseStatusListFragment exten
return attachmentViewsPool;
}
+ public void rebuildAllDisplayItems(){
+ displayItems.clear();
+ for(T item:data){
+ displayItems.addAll(buildDisplayItems(item));
+ }
+ adapter.notifyDataSetChanged();
+ }
+
protected void onModifyItemViewHolder(BindableViewHolder holder){}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
index 5786595c..037e0816 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
@@ -46,6 +46,7 @@ import android.widget.TextView;
import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
+import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
@@ -546,7 +547,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.publish){
- publish();
+ if(GlobalUserPreferences.altTextReminders)
+ checkAltTextsAndPublish();
+ else
+ publish();
}
return true;
}
@@ -641,6 +645,28 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return true;
}
+ private void checkAltTextsAndPublish(){
+ int count=mediaViewController.getMissingAltTextAttachmentCount();
+ if(count==0){
+ publish();
+ }else{
+ String msg=getResources().getQuantityString(mediaViewController.areAllAttachmentsImages() ? R.plurals.alt_text_reminder_x_images : R.plurals.alt_text_reminder_x_attachments,
+ count, switch(count){
+ case 1 -> getString(R.string.count_one);
+ case 2 -> getString(R.string.count_two);
+ case 3 -> getString(R.string.count_three);
+ case 4 -> getString(R.string.count_four);
+ default -> String.valueOf(count);
+ });
+ new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.alt_text_reminder_title)
+ .setMessage(msg)
+ .setPositiveButton(R.string.alt_text_reminder_post_anyway, (dlg, item)->publish())
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+ }
+
private void publish(){
sendingOverlay=new View(getActivity());
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
@@ -655,7 +681,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publishButton.setEnabled(false);
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
-
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
index 7684c571..4fa683f0 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
@@ -3,14 +3,11 @@ package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.NotificationManager;
-import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
-import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.FrameLayout;
@@ -20,19 +17,19 @@ import android.widget.TextView;
import com.squareup.otto.Subscribe;
+import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R;
-import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
+import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
-import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -265,7 +262,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
new AccountSwitcherSheet(getActivity(), this).show();
return true;
}
- if(tab==R.id.tab_home){
+ if(tab==R.id.tab_home && BuildConfig.DEBUG){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
@@ -328,7 +325,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
notificationsBadge.setVisibility(View.GONE);
}else{
notificationsBadge.setVisibility(View.VISIBLE);
- if(notifications.get(notifications.size()-1).id.compareTo(marker)<=0){
+ if(notifications.get(notifications.size()-1).id.compareTo(marker)>0){
notificationsBadge.setText(String.format("%d+", notifications.size()));
}else{
int count=0;
@@ -349,4 +346,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(ev.clearUnread)
notificationsBadge.setVisibility(View.GONE);
}
+
+ @Subscribe
+ public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
+ if(!ev.accountID.equals(accountID))
+ return;
+ if(homeTimelineFragment.loaded)
+ homeTimelineFragment.rebuildAllDisplayItems();
+ if(notificationsFragment.loaded)
+ notificationsFragment.rebuildAllDisplayItems();
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java
index 67c88be8..453590d1 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java
@@ -30,8 +30,10 @@ import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
+import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
@@ -123,7 +125,7 @@ public class HomeTimelineFragment extends StatusListFragment{
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
- Nav.go(getActivity(), SettingsFragment.class, args);
+ Nav.go(getActivity(), SettingsMainFragment.class, args);
return true;
}
@@ -200,7 +202,7 @@ public class HomeTimelineFragment extends StatusListFragment{
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
- List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
+ List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
if(!filters.isEmpty()){
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
}
@@ -277,12 +279,12 @@ public class HomeTimelineFragment extends StatusListFragment{
List targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
- List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
+ List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
outer:
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(s)){
continue outer;
}
@@ -444,6 +446,11 @@ public class HomeTimelineFragment extends StatusListFragment{
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
}
+ @Override
+ protected boolean wantsToolbarMenuIconsTinted(){
+ return false;
+ }
+
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateState(ev.state);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
index 125f5a2a..68aecec3 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
@@ -315,10 +315,12 @@ public class NotificationsListFragment extends BaseStatusListFragment0){
+ new SaveMarkers(null, id).exec(accountID);
+ AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
+ realUnreadMarker=id;
+ updateMarkAllReadButton();
+ }
}
private void resetUnreadBackground(){
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
index 3a98b3d5..efc59ce5 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
@@ -458,7 +458,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
- HtmlParser.parseCustomEmoji(ssb, account.emojis);
+ if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
+ HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java
deleted file mode 100644
index f7d4ac8d..00000000
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java
+++ /dev/null
@@ -1,761 +0,0 @@
-package org.joinmastodon.android.fragments;
-
-import android.animation.ObjectAnimator;
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.animation.LinearInterpolator;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.PopupMenu;
-import android.widget.ProgressBar;
-import android.widget.RadioButton;
-import android.widget.Switch;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.squareup.otto.Subscribe;
-
-import org.joinmastodon.android.BuildConfig;
-import org.joinmastodon.android.E;
-import org.joinmastodon.android.GlobalUserPreferences;
-import org.joinmastodon.android.MainActivity;
-import org.joinmastodon.android.MastodonApp;
-import org.joinmastodon.android.R;
-import org.joinmastodon.android.api.MastodonAPIController;
-import org.joinmastodon.android.api.PushSubscriptionManager;
-import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
-import org.joinmastodon.android.api.session.AccountActivationInfo;
-import org.joinmastodon.android.api.session.AccountSession;
-import org.joinmastodon.android.api.session.AccountSessionManager;
-import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
-import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
-import org.joinmastodon.android.model.PushNotification;
-import org.joinmastodon.android.model.PushSubscription;
-import org.joinmastodon.android.ui.M3AlertDialogBuilder;
-import org.joinmastodon.android.ui.OutlineProviders;
-import org.joinmastodon.android.ui.utils.UiUtils;
-import org.joinmastodon.android.updater.GithubSelfUpdater;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import me.grishka.appkit.Nav;
-import me.grishka.appkit.api.Callback;
-import me.grishka.appkit.api.ErrorResponse;
-import me.grishka.appkit.imageloader.ImageCache;
-import me.grishka.appkit.utils.BindableViewHolder;
-import me.grishka.appkit.utils.V;
-import me.grishka.appkit.views.UsableRecyclerView;
-
-public class SettingsFragment extends MastodonToolbarFragment{
- private UsableRecyclerView list;
- private ArrayList- items=new ArrayList<>();
- private ThemeItem themeItem;
- private NotificationPolicyItem notificationPolicyItem;
- private String accountID;
- private boolean needUpdateNotificationSettings;
- private PushSubscription pushSubscription;
-
- private ImageView themeTransitionWindowView;
-
- @Override
- public void onCreate(Bundle savedInstanceState){
- super.onCreate(savedInstanceState);
- if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
- setRetainInstance(true);
- setTitle(R.string.settings);
- accountID=getArguments().getString("account");
- AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
-
- if(GithubSelfUpdater.needSelfUpdating()){
- GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
- GithubSelfUpdater.UpdateState state=updater.getState();
- if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
- items.add(new UpdateItem());
- }
- }
-
- items.add(new HeaderItem(R.string.settings_theme));
- items.add(themeItem=new ThemeItem());
- items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
-
- items.add(new HeaderItem(R.string.settings_behavior));
- items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
- GlobalUserPreferences.playGifs=i.checked;
- GlobalUserPreferences.save();
- }));
- items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
- GlobalUserPreferences.useCustomTabs=i.checked;
- GlobalUserPreferences.save();
- }));
-
- items.add(new HeaderItem(R.string.settings_notifications));
- items.add(notificationPolicyItem=new NotificationPolicyItem());
- PushSubscription pushSubscription=getPushSubscription();
- items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
- items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
- items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
- items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
-
- items.add(new HeaderItem(R.string.settings_boring));
- items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
- items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
- items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
- items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
-
- items.add(new RedHeaderItem(R.string.settings_spicy));
- items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
- items.add(new TextItem(R.string.log_out, this::confirmLogOut));
-
- if(BuildConfig.DEBUG){
- items.add(new RedHeaderItem("Debug options"));
- items.add(new TextItem("Test e-mail confirmation flow", ()->{
- AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
- sess.activated=false;
- sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
- Bundle args=new Bundle();
- args.putString("account", accountID);
- args.putBoolean("debug", true);
- Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
- }));
- }
-
- items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
- }
-
- @Override
- public void onAttach(Activity activity){
- super.onAttach(activity);
- if(themeTransitionWindowView!=null){
- // Activity has finished recreating. Remove the overlay.
- MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
- themeTransitionWindowView=null;
- }
- }
-
- @Override
- public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
- list=new UsableRecyclerView(getActivity());
- list.setLayoutManager(new LinearLayoutManager(getActivity()));
- list.setAdapter(new SettingsAdapter());
- list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
- list.setPadding(0, V.dp(16), 0, V.dp(12));
- list.setClipToPadding(false);
- list.addItemDecoration(new RecyclerView.ItemDecoration(){
- @Override
- public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
- // Add 32dp gaps between sections
- RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
- if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
- outRect.top=V.dp(32);
- }
- });
- return list;
- }
-
- @Override
- public void onApplyWindowInsets(WindowInsets insets){
- if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
- list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
- insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
- }
- super.onApplyWindowInsets(insets);
- }
-
- @Override
- public void onDestroy(){
- super.onDestroy();
- if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
- AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
- }
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState){
- super.onViewCreated(view, savedInstanceState);
- if(GithubSelfUpdater.needSelfUpdating())
- E.register(this);
- }
-
- @Override
- public void onDestroyView(){
- super.onDestroyView();
- if(GithubSelfUpdater.needSelfUpdating())
- E.unregister(this);
- }
-
- private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
- GlobalUserPreferences.theme=theme;
- GlobalUserPreferences.save();
- restartActivityToApplyNewTheme();
- }
-
- private void onTrueBlackThemeChanged(SwitchItem item){
- GlobalUserPreferences.trueBlackTheme=item.checked;
- GlobalUserPreferences.save();
-
- RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
- if(themeHolder!=null){
- ((ThemeViewHolder)themeHolder).bindSubitems();
- }else{
- list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
- }
-
- if(UiUtils.isDarkTheme()){
- restartActivityToApplyNewTheme();
- }
- }
-
- private void restartActivityToApplyNewTheme(){
- // Calling activity.recreate() causes a black screen for like half a second.
- // So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
- // As a bonus, we can fade it out to make it even smoother.
- if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
- View activityDecorView=getActivity().getWindow().getDecorView();
- Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
- activityDecorView.draw(new Canvas(bitmap));
- themeTransitionWindowView=new ImageView(MastodonApp.context);
- themeTransitionWindowView.setImageBitmap(bitmap);
- WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
- lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
- WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
- lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
- lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
- lp.token=getActivity().getWindow().getAttributes().token;
- lp.windowAnimations=R.style.window_fade_out;
- MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
- }
- getActivity().recreate();
- }
-
- private PushSubscription getPushSubscription(){
- if(pushSubscription!=null)
- return pushSubscription;
- AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
- if(session.pushSubscription==null){
- pushSubscription=new PushSubscription();
- pushSubscription.alerts=PushSubscription.Alerts.ofAll();
- }else{
- pushSubscription=session.pushSubscription.clone();
- }
- return pushSubscription;
- }
-
- private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
- PushSubscription subscription=getPushSubscription();
- switch(type){
- case FAVORITE -> subscription.alerts.favourite=enabled;
- case FOLLOW -> subscription.alerts.follow=enabled;
- case REBLOG -> subscription.alerts.reblog=enabled;
- case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
- }
- needUpdateNotificationSettings=true;
- }
-
- private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
- PushSubscription subscription=getPushSubscription();
- PushSubscription.Policy prevPolicy=subscription.policy;
- if(prevPolicy==policy)
- return;
- subscription.policy=policy;
- int index=items.indexOf(notificationPolicyItem);
- RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
- if(policyHolder!=null){
- ((NotificationPolicyViewHolder)policyHolder).rebind();
- }else{
- list.getAdapter().notifyItemChanged(index);
- }
- if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
- index++;
- while(items.get(index) instanceof SwitchItem si){
- si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
- RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
- if(holder!=null)
- ((BindableViewHolder>)holder).rebind();
- else
- list.getAdapter().notifyItemChanged(index);
- index++;
- }
- }
- needUpdateNotificationSettings=true;
- }
-
- private void confirmLogOut(){
- new M3AlertDialogBuilder(getActivity())
- .setTitle(R.string.log_out)
- .setMessage(R.string.confirm_log_out)
- .setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
- .setNegativeButton(R.string.cancel, null)
- .show();
- }
-
- private void logOut(){
- AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
- new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
- .setCallback(new Callback<>(){
- @Override
- public void onSuccess(Object result){
- onLoggedOut();
- }
-
- @Override
- public void onError(ErrorResponse error){
- onLoggedOut();
- }
- })
- .wrapProgress(getActivity(), R.string.loading, false)
- .exec(accountID);
- }
-
- private void onLoggedOut(){
- AccountSessionManager.getInstance().removeAccount(accountID);
- getActivity().finish();
- Intent intent=new Intent(getActivity(), MainActivity.class);
- startActivity(intent);
- }
-
- private void clearImageCache(){
- MastodonAPIController.runInBackground(()->{
- Activity activity=getActivity();
- ImageCache.getInstance(getActivity()).clear();
- Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
- });
- }
-
- @Subscribe
- public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
- if(items.get(0) instanceof UpdateItem item){
- RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
- if(holder instanceof UpdateViewHolder uvh){
- uvh.bind(item);
- }
- }
- }
-
- private static abstract class Item{
- public abstract int getViewType();
- }
-
- private class HeaderItem extends Item{
- private String text;
-
- public HeaderItem(@StringRes int text){
- this.text=getString(text);
- }
-
- public HeaderItem(String text){
- this.text=text;
- }
-
- @Override
- public int getViewType(){
- return 0;
- }
- }
-
- private class SwitchItem extends Item{
- private String text;
- private int icon;
- private boolean checked;
- private Consumer onChanged;
- private boolean enabled=true;
-
- public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer onChanged){
- this.text=getString(text);
- this.icon=icon;
- this.checked=checked;
- this.onChanged=onChanged;
- }
-
- public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer onChanged, boolean enabled){
- this.text=getString(text);
- this.icon=icon;
- this.checked=checked;
- this.onChanged=onChanged;
- this.enabled=enabled;
- }
-
- @Override
- public int getViewType(){
- return 1;
- }
- }
-
- private static class ThemeItem extends Item{
-
- @Override
- public int getViewType(){
- return 2;
- }
- }
-
- private static class NotificationPolicyItem extends Item{
-
- @Override
- public int getViewType(){
- return 3;
- }
- }
-
- private class TextItem extends Item{
- private String text;
- private Runnable onClick;
-
- public TextItem(@StringRes int text, Runnable onClick){
- this.text=getString(text);
- this.onClick=onClick;
- }
-
- public TextItem(String text, Runnable onClick){
- this.text=text;
- this.onClick=onClick;
- }
-
- @Override
- public int getViewType(){
- return 4;
- }
- }
-
- private class RedHeaderItem extends HeaderItem{
-
- public RedHeaderItem(int text){
- super(text);
- }
-
- public RedHeaderItem(String text){
- super(text);
- }
-
- @Override
- public int getViewType(){
- return 5;
- }
- }
-
- private class FooterItem extends Item{
- private String text;
-
- public FooterItem(String text){
- this.text=text;
- }
-
- @Override
- public int getViewType(){
- return 6;
- }
- }
-
- private class UpdateItem extends Item{
-
- @Override
- public int getViewType(){
- return 7;
- }
- }
-
- private class SettingsAdapter extends RecyclerView.Adapter>{
- @NonNull
- @Override
- public BindableViewHolder
- onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
- //noinspection unchecked
- return (BindableViewHolder
- ) switch(viewType){
- case 0 -> new HeaderViewHolder(false);
- case 1 -> new SwitchViewHolder();
- case 2 -> new ThemeViewHolder();
- case 3 -> new NotificationPolicyViewHolder();
- case 4 -> new TextViewHolder();
- case 5 -> new HeaderViewHolder(true);
- case 6 -> new FooterViewHolder();
- case 7 -> new UpdateViewHolder();
- default -> throw new IllegalStateException("Unexpected value: "+viewType);
- };
- }
-
- @Override
- public void onBindViewHolder(@NonNull BindableViewHolder
- holder, int position){
- holder.bind(items.get(position));
- }
-
- @Override
- public int getItemCount(){
- return items.size();
- }
-
- @Override
- public int getItemViewType(int position){
- return items.get(position).getViewType();
- }
- }
-
- private class HeaderViewHolder extends BindableViewHolder{
- private final TextView text;
- public HeaderViewHolder(boolean red){
- super(getActivity(), R.layout.item_settings_header, list);
- text=(TextView) itemView;
- if(red)
- text.setTextColor(getResources().getColor(UiUtils.isDarkTheme() ? R.color.error_400 : R.color.error_700));
- }
-
- @Override
- public void onBind(HeaderItem item){
- text.setText(item.text);
- }
- }
-
- private class SwitchViewHolder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{
- private final TextView text;
- private final ImageView icon;
- private final Switch checkbox;
-
- public SwitchViewHolder(){
- super(getActivity(), R.layout.item_settings_switch, list);
- text=findViewById(R.id.text);
- icon=findViewById(R.id.icon);
- checkbox=findViewById(R.id.checkbox);
- }
-
- @Override
- public void onBind(SwitchItem item){
- text.setText(item.text);
- icon.setImageResource(item.icon);
- checkbox.setChecked(item.checked && item.enabled);
- checkbox.setEnabled(item.enabled);
- }
-
- @Override
- public void onClick(){
- item.checked=!item.checked;
- checkbox.setChecked(item.checked);
- item.onChanged.accept(item);
- }
-
- @Override
- public boolean isEnabled(){
- return item.enabled;
- }
- }
-
- private class ThemeViewHolder extends BindableViewHolder{
- private SubitemHolder autoHolder, lightHolder, darkHolder;
-
- public ThemeViewHolder(){
- super(getActivity(), R.layout.item_settings_theme, list);
- autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
- lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
- darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
- }
-
- @Override
- public void onBind(ThemeItem item){
- bindSubitems();
- }
-
- public void bindSubitems(){
- autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
- lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
- darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
- }
-
- private void onSubitemClick(View v){
- GlobalUserPreferences.ThemePreference pref;
- if(v.getId()==R.id.theme_auto)
- pref=GlobalUserPreferences.ThemePreference.AUTO;
- else if(v.getId()==R.id.theme_light)
- pref=GlobalUserPreferences.ThemePreference.LIGHT;
- else if(v.getId()==R.id.theme_dark)
- pref=GlobalUserPreferences.ThemePreference.DARK;
- else
- return;
- onThemePreferenceClick(pref);
- }
-
- private class SubitemHolder{
- public TextView text;
- public ImageView icon;
- public RadioButton checkbox;
-
- public SubitemHolder(View view){
- text=view.findViewById(R.id.text);
- icon=view.findViewById(R.id.icon);
- checkbox=view.findViewById(R.id.checkbox);
- view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
-
- icon.setClipToOutline(true);
- icon.setOutlineProvider(OutlineProviders.roundedRect(4));
- }
-
- public void bind(int text, int icon, boolean checked){
- this.text.setText(text);
- this.icon.setImageResource(icon);
- checkbox.setChecked(checked);
- }
-
- public void setChecked(boolean checked){
- checkbox.setChecked(checked);
- }
- }
- }
-
- private class NotificationPolicyViewHolder extends BindableViewHolder{
- private final Button button;
- private final PopupMenu popupMenu;
-
- @SuppressLint("ClickableViewAccessibility")
- public NotificationPolicyViewHolder(){
- super(getActivity(), R.layout.item_settings_notification_policy, list);
- button=findViewById(R.id.button);
- popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
- popupMenu.inflate(R.menu.notification_policy);
- popupMenu.setOnMenuItemClickListener(item->{
- PushSubscription.Policy policy;
- int id=item.getItemId();
- if(id==R.id.notify_anyone)
- policy=PushSubscription.Policy.ALL;
- else if(id==R.id.notify_followed)
- policy=PushSubscription.Policy.FOLLOWED;
- else if(id==R.id.notify_follower)
- policy=PushSubscription.Policy.FOLLOWER;
- else if(id==R.id.notify_none)
- policy=PushSubscription.Policy.NONE;
- else
- return false;
- onNotificationsPolicyChanged(policy);
- return true;
- });
- UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
- button.setOnTouchListener(popupMenu.getDragToOpenListener());
- button.setOnClickListener(v->popupMenu.show());
- }
-
- @Override
- public void onBind(NotificationPolicyItem item){
- button.setText(switch(getPushSubscription().policy){
- case ALL -> R.string.notify_anyone;
- case FOLLOWED -> R.string.notify_followed;
- case FOLLOWER -> R.string.notify_follower;
- case NONE -> R.string.notify_none;
- });
- }
- }
-
- private class TextViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{
- private final TextView text;
- public TextViewHolder(){
- super(getActivity(), R.layout.item_settings_text, list);
- text=(TextView) itemView;
- }
-
- @Override
- public void onBind(TextItem item){
- text.setText(item.text);
- }
-
- @Override
- public void onClick(){
- item.onClick.run();
- }
- }
-
- private class FooterViewHolder extends BindableViewHolder{
- private final TextView text;
- public FooterViewHolder(){
- super(getActivity(), R.layout.item_settings_footer, list);
- text=(TextView) itemView;
- }
-
- @Override
- public void onBind(FooterItem item){
- text.setText(item.text);
- }
- }
-
- private class UpdateViewHolder extends BindableViewHolder{
-
- private final TextView text;
- private final Button button;
- private final ImageButton cancelBtn;
- private final ProgressBar progress;
-
- private ObjectAnimator rotationAnimator;
- private Runnable progressUpdater=this::updateProgress;
-
- public UpdateViewHolder(){
- super(getActivity(), R.layout.item_settings_update, list);
- text=findViewById(R.id.text);
- button=findViewById(R.id.button);
- cancelBtn=findViewById(R.id.cancel_btn);
- progress=findViewById(R.id.progress);
- button.setOnClickListener(v->{
- GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
- switch(updater.getState()){
- case UPDATE_AVAILABLE -> updater.downloadUpdate();
- case DOWNLOADED -> updater.installUpdate(getActivity());
- }
- });
- cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
- rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
- rotationAnimator.setInterpolator(new LinearInterpolator());
- rotationAnimator.setDuration(1500);
- rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
- }
-
- @Override
- public void onBind(UpdateItem item){
- GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
- GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
- GithubSelfUpdater.UpdateState state=updater.getState();
- if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
- text.setText(getString(R.string.update_available, info.version));
- button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
- }else{
- text.setText(getString(R.string.update_ready, info.version));
- button.setText(R.string.install_update);
- }
- if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
- rotationAnimator.start();
- button.setVisibility(View.INVISIBLE);
- cancelBtn.setVisibility(View.VISIBLE);
- progress.setVisibility(View.VISIBLE);
- updateProgress();
- }else{
- rotationAnimator.cancel();
- button.setVisibility(View.VISIBLE);
- cancelBtn.setVisibility(View.GONE);
- progress.setVisibility(View.GONE);
- progress.removeCallbacks(progressUpdater);
- }
- }
-
- private void updateProgress(){
- GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
- if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
- return;
- int value=Math.round(progress.getMax()*updater.getDownloadProgress());
- if(Build.VERSION.SDK_INT>=24)
- progress.setProgress(value, true);
- else
- progress.setProgress(value);
- progress.postDelayed(progressUpdater, 1000);
- }
- }
-}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
index c2d4087d..ac069919 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
@@ -1,8 +1,6 @@
package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -13,7 +11,8 @@ import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
@@ -47,7 +46,10 @@ public class ThreadFragment extends StatusListFragment{
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
- setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
+ if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
+ setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
+ else
+ setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
}
@Override
@@ -102,11 +104,11 @@ public class ThreadFragment extends StatusListFragment{
}
private List filterStatuses(List statuses){
- List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
+ List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.THREAD)).collect(Collectors.toList());
if(filters.isEmpty())
return statuses;
return statuses.stream().filter(status->{
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(status))
return false;
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java
index 811beda4..aa0fb853 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/ComposeAccountSearchFragment.java
@@ -95,7 +95,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
- onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false);
+ onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
})
.exec(accountID);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java
index 0c4a0c43..1bf665fb 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java
@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
- onDataLoaded(result.stream().map(AccountViewModel::new).collect(Collectors.toList()), nextMaxID!=null);
+ onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
index 76c804c3..2d3ec3c2 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
@@ -5,7 +5,7 @@ import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -27,7 +27,7 @@ public class LocalTimelineFragment extends StatusListFragment{
public void onSuccess(List result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
- onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
+ onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
}
})
.exec(accountID);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java
index c72b028a..d54942fe 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
-import org.joinmastodon.android.fragments.HomeFragment;
-import org.joinmastodon.android.fragments.SettingsFragment;
+import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
-import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends ToolbarFragment{
private String accountID;
@@ -70,7 +67,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
- Nav.go(getActivity(), SettingsFragment.class, args);
+ Nav.go(getActivity(), SettingsMainFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java
index 47e07d4d..8b6583c7 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceRulesFragment.java
@@ -1,8 +1,6 @@
package org.joinmastodon.android.fragments.onboarding;
-import android.annotation.SuppressLint;
import android.app.Activity;
-import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
@@ -15,20 +13,16 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
-import org.joinmastodon.android.ui.text.HtmlParser;
+import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
-import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.ToolbarFragment;
-import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
-import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
@@ -68,9 +62,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
- adapter.addAdapter(new ItemsAdapter());
+ adapter.addAdapter(new InstanceRulesAdapter(instance.rules));
list.setAdapter(adapter);
- list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
@@ -113,43 +106,4 @@ public class InstanceRulesFragment extends ToolbarFragment{
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
-
- private class ItemsAdapter extends RecyclerView.Adapter{
-
- @NonNull
- @Override
- public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
- return new ItemViewHolder();
- }
-
- @Override
- public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
- holder.bind(instance.rules.get(position));
- }
-
- @Override
- public int getItemCount(){
- return instance.rules.size();
- }
- }
-
- private class ItemViewHolder extends BindableViewHolder{
- private final TextView text, number;
-
- public ItemViewHolder(){
- super(getActivity(), R.layout.item_server_rule, list);
- text=findViewById(R.id.text);
- number=findViewById(R.id.number);
- }
-
- @SuppressLint("DefaultLocale")
- @Override
- public void onBind(Instance.Rule item){
- if(item.parsedText==null){
- item.parsedText=HtmlParser.parseLinks(item.text);
- }
- text.setText(item.parsedText);
- number.setText(String.format("%d", getAbsoluteAdapterPosition()));
- }
- }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java
index 64827084..30c4741e 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java
@@ -22,8 +22,8 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.FollowSuggestion;
-import org.joinmastodon.android.model.ParsedAccount;
import org.joinmastodon.android.model.Relationship;
+import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
@@ -52,7 +52,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
-public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment{
+public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment{
private String accountID;
private Map relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -97,7 +97,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment(this){
@Override
public void onSuccess(List result){
- onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
+ onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
loadRelationships();
}
})
@@ -146,7 +146,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment accountIdsToFollow=new ArrayList<>();
- for(ParsedAccount acc:data){
+ for(AccountViewModel acc:data){
Relationship rel=relationships.get(acc.account.id);
if(rel==null)
continue;
@@ -239,14 +239,14 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
+ private class SuggestionViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, bio;
private final ImageView avatar;
private final ProgressBarButton actionButton;
@@ -271,7 +271,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment extends MastodonRecyclerFragment>{
+ protected GenericListItemsAdapter itemsAdapter;
+ protected String accountID;
+
+ public BaseSettingsFragment(){
+ super(20);
+ }
+
+ public BaseSettingsFragment(int perPage){
+ super(perPage);
+ }
+
+ public BaseSettingsFragment(int layout, int perPage){
+ super(layout, perPage);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ accountID=getArguments().getString("account");
+ setRefreshEnabled(false);
+ }
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ return itemsAdapter=new GenericListItemsAdapter(data);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState){
+ super.onViewCreated(view, savedInstanceState);
+ list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter));
+ list.setItemAnimator(new BetterItemAnimator());
+ }
+
+ protected int indexOfItemsAdapter(){
+ return 0;
+ }
+
+ protected void toggleCheckableItem(CheckableListItem item){
+ item.toggle();
+ rebindItem(item);
+ }
+
+ protected void rebindItem(ListItem item){
+ if(list==null)
+ return;
+ if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder> holder){
+ holder.rebind();
+ }
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets){
+ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
+ list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
+ }else{
+ list.setPadding(0, 0, 0, 0);
+ }
+ super.onApplyWindowInsets(insets);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java
new file mode 100644
index 00000000..783f9af6
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java
@@ -0,0 +1,324 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.DatePicker;
+import android.widget.EditText;
+
+import org.joinmastodon.android.E;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.MastodonAPIRequest;
+import org.joinmastodon.android.api.requests.filters.CreateFilter;
+import org.joinmastodon.android.api.requests.filters.DeleteFilter;
+import org.joinmastodon.android.api.requests.filters.UpdateFilter;
+import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
+import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
+import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterAction;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.FilterKeyword;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
+import org.parceler.Parcels;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.Nav;
+import me.grishka.appkit.api.APIRequest;
+import me.grishka.appkit.api.Callback;
+import me.grishka.appkit.api.ErrorResponse;
+import me.grishka.appkit.fragments.OnBackPressedListener;
+import me.grishka.appkit.utils.MergeRecyclerAdapter;
+import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
+
+public class EditFilterFragment extends BaseSettingsFragment implements OnBackPressedListener{
+ private static final int WORDS_RESULT=370;
+ private static final int CONTEXT_RESULT=651;
+
+ private Filter filter;
+ private ListItem durationItem, wordsItem, contextItem;
+ private CheckableListItem cwItem;
+ private FloatingHintEditTextLayout titleEditLayout;
+ private EditText titleEdit;
+
+ private Instant endsAt;
+ private ArrayList keywords=new ArrayList<>();
+ private ArrayList deletedWordIDs=new ArrayList<>();
+ private EnumSet context=EnumSet.allOf(FilterContext.class);
+ private boolean dirty;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ filter=Parcels.unwrap(getArguments().getParcelable("filter"));
+ setTitle(filter==null ? R.string.settings_add_filter : R.string.settings_edit_filter);
+ onDataLoaded(List.of(
+ durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
+ wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
+ contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
+ cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
+ ));
+
+ if(filter!=null){
+ endsAt=filter.expiresAt;
+ keywords.addAll(filter.keywords);
+ context=filter.context;
+ data.add(new ListItem<>(R.string.settings_delete_filter, 0, this::onDeleteClick, R.attr.colorM3Error, false));
+ }
+
+ updateDurationItem();
+ updateWordsItem();
+ updateContextItem();
+ setHasOptionsMenu(true);
+ setRetainInstance(true);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, list, false);
+ titleEdit=titleEditLayout.findViewById(R.id.edit);
+ titleEdit.setHint(R.string.settings_filter_title);
+ titleEditLayout.updateHint();
+ if(filter!=null)
+ titleEdit.setText(filter.title);
+
+ MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
+ adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
+ adapter.addAdapter(super.getAdapter());
+ return adapter;
+ }
+
+ @Override
+ protected int indexOfItemsAdapter(){
+ return 1;
+ }
+
+ private void onDurationClick(){
+ int[] durationOptions={
+ 1800,
+ 3600,
+ 6*3600,
+ 12*3600,
+ 24*3600,
+ 3*24*3600,
+ 7*24*3600
+ };
+ ArrayList options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList::new));
+ options.add(0, getString(R.string.filter_duration_forever));
+ options.add(getString(R.string.filter_duration_custom));
+ Instant[] newEnd={null};
+ AlertDialog alert=new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.settings_filter_duration_title)
+ .setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
+ .setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
+ AlertDialog a=(AlertDialog) dlg;
+ if(item==options.size()-1){ // custom
+ showCustomDurationAlert(date->{
+ if(date==null){
+ a.getListView().setItemChecked(item, false);
+ }else{
+ newEnd[0]=date;
+ a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
+ }
+ });
+ }else{
+ if(item==0){
+ newEnd[0]=null;
+ }else{
+ newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
+ }
+ a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
+ }
+ })
+ .setPositiveButton(R.string.ok, (dlg, item)->{
+ if(!Objects.equals(endsAt, newEnd[0])){
+ endsAt=newEnd[0];
+ updateDurationItem();
+ dirty=true;
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ }
+
+ private void showCustomDurationAlert(Consumer callback){
+ DatePicker picker=new DatePicker(getActivity());
+ picker.setMinDate(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond()*1000L);
+ AlertDialog alert=new M3AlertDialogBuilder(getActivity())
+ .setView(picker)
+ .setPositiveButton(R.string.ok, (dlg, item)->{
+ ((AlertDialog)dlg).setOnDismissListener(null);
+ callback.accept(LocalDate.of(picker.getYear(), picker.getMonth()+1, picker.getDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ alert.setOnDismissListener(dialog->callback.accept(null));
+ }
+
+ private void onWordsClick(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putParcelableArrayList("words", (ArrayList extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
+ Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
+ }
+
+ private void onContextClick(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putSerializable("context", context);
+ Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
+ }
+
+ private void onDeleteClick(){
+ AlertDialog alert=new M3AlertDialogBuilder(getActivity())
+ .setTitle(getString(R.string.settings_delete_filter_title, filter.title))
+ .setMessage(R.string.settings_delete_filter_confirmation)
+ .setPositiveButton(R.string.delete, (dlg, item)->deleteFilter())
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
+ }
+
+ private void updateDurationItem(){
+ if(endsAt==null){
+ durationItem.subtitle=getString(R.string.filter_duration_forever);
+ }else{
+ durationItem.subtitle=getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false));
+ }
+ rebindItem(durationItem);
+ }
+
+ private void updateWordsItem(){
+ wordsItem.subtitle=getResources().getQuantityString(R.plurals.settings_x_muted_words, keywords.size(), keywords.size());
+ rebindItem(wordsItem);
+ }
+
+ private void updateContextItem(){
+ List values=context.stream().map(c->getString(c.getDisplayNameRes())).collect(Collectors.toList());
+ contextItem.subtitle=switch(values.size()){
+ case 0 -> null;
+ case 1 -> values.get(0);
+ case 2 -> getString(R.string.selection_2_options, values.get(0), values.get(1));
+ case 3 -> getString(R.string.selection_3_options, values.get(0), values.get(1), values.get(2));
+ default -> getString(R.string.selection_4_or_more, values.get(0), values.get(1), values.size()-2);
+ };
+ rebindItem(contextItem);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
+ inflater.inflate(R.menu.settings_edit_filter, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ if(item.getItemId()==R.id.save){
+ saveFilter();
+ }
+ return true;
+ }
+
+ private void saveFilter(){
+ if(titleEdit.length()==0){
+ titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
+ return;
+ }
+ MastodonAPIRequest req;
+ if(filter==null){
+ req=new CreateFilter(titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords);
+ }else{
+ req=new UpdateFilter(filter.id, titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords, deletedWordIDs);
+ }
+ req.setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Filter result){
+ E.post(new SettingsFilterCreatedOrUpdatedEvent(accountID, result));
+ Nav.finish(EditFilterFragment.this);
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ error.showToast(getActivity());
+ }
+ })
+ .wrapProgress(getActivity(), R.string.saving, true)
+ .exec(accountID);
+ }
+
+ private void deleteFilter(){
+ new DeleteFilter(filter.id)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Void result){
+ E.post(new SettingsFilterDeletedEvent(accountID, filter.id));
+ Nav.finish(EditFilterFragment.this);
+ }
+
+ @Override
+ public void onError(ErrorResponse error){
+ error.showToast(getActivity());
+ }
+ })
+ .wrapProgress(getActivity(), R.string.deleting, false)
+ .exec(accountID);
+ }
+
+ @Override
+ public void onFragmentResult(int reqCode, boolean success, Bundle result){
+ if(success){
+ if(reqCode==CONTEXT_RESULT){
+ EnumSet context=(EnumSet) result.getSerializable("context");
+ if(!context.equals(this.context)){
+ this.context=context;
+ dirty=true;
+ updateContextItem();
+ }
+ }else if(reqCode==WORDS_RESULT){
+ ArrayList old=new ArrayList<>(keywords);
+ keywords.clear();
+ result.getParcelableArrayList("words").stream().map(p->(FilterKeyword)Parcels.unwrap(p)).forEach(keywords::add);
+ if(!old.equals(keywords)){
+ dirty=true;
+ updateWordsItem();
+ }
+ deletedWordIDs.addAll(result.getStringArrayList("deleted"));
+ }
+ }
+ }
+
+ private boolean isDirty(){
+ return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title));
+ }
+
+ @Override
+ public boolean onBackPressed(){
+ if(isDirty()){
+ UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java
new file mode 100644
index 00000000..233c00cb
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java
@@ -0,0 +1,48 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.os.Bundle;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.stream.Collectors;
+
+import me.grishka.appkit.fragments.OnBackPressedListener;
+
+public class FilterContextFragment extends BaseSettingsFragment implements OnBackPressedListener{
+ private EnumSet context;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_filter_context);
+ context=(EnumSet) getArguments().getSerializable("context");
+ onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
+ CheckableListItem item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
+ item.parentObject=c;
+ item.isEnabled=true;
+ item.onClick=()->toggleCheckableItem(item);
+ return item;
+ }).collect(Collectors.toList()));
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ public boolean onBackPressed(){
+ context=EnumSet.noneOf(FilterContext.class);
+ for(ListItem item:data){
+ if(((CheckableListItem) item).checked)
+ context.add(item.parentObject);
+ }
+ Bundle args=new Bundle();
+ args.putSerializable("context", context);
+ setResult(true, args);
+ return false;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java
new file mode 100644
index 00000000..52c4f195
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java
@@ -0,0 +1,327 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.IntEvaluator;
+import android.animation.ObjectAnimator;
+import android.app.AlertDialog;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.InputType;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.FilterKeyword;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
+import org.parceler.Parcels;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import me.grishka.appkit.FragmentStackActivity;
+import me.grishka.appkit.fragments.OnBackPressedListener;
+import me.grishka.appkit.utils.V;
+
+public class FilterWordsFragment extends BaseSettingsFragment implements OnBackPressedListener{
+ private ImageButton fab;
+ private ActionMode actionMode;
+ private ArrayList> selectedItems=new ArrayList<>();
+ private ArrayList deletedItemIDs=new ArrayList<>();
+ private MenuItem deleteItem;
+
+ public FilterWordsFragment(){
+ setListLayoutId(R.layout.recycler_fragment_with_fab);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_filter_muted_words);
+ onDataLoaded(getArguments().getParcelableArrayList("words").stream().map(p->{
+ FilterKeyword word=Parcels.unwrap(p);
+ ListItem item=new ListItem<>(word.keyword, null, null, word);
+ item.isEnabled=true;
+ item.onClick=()->onWordClick(item);
+ return item;
+ }).collect(Collectors.toList()));
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ private void onWordClick(ListItem item){
+ showAlertForWord(item.parentObject);
+ }
+
+ private void onSelectionModeWordClick(CheckableListItem item){
+ if(selectedItems.remove(item)){
+ item.checked=false;
+ }else{
+ item.checked=true;
+ selectedItems.add(item);
+ }
+ rebindItem(item);
+ updateActionModeTitle();
+ }
+
+ @Override
+ public boolean onBackPressed(){
+ Bundle result=new Bundle();
+ result.putParcelableArrayList("words", (ArrayList extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
+ result.putStringArrayList("deleted", deletedItemIDs);
+ setResult(true, result);
+ return false;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState){
+ super.onViewCreated(view, savedInstanceState);
+ fab=view.findViewById(R.id.fab);
+ fab.setImageResource(R.drawable.ic_add_24px);
+ fab.setContentDescription(getString(R.string.add_muted_word));
+ fab.setOnClickListener(v->onFabClick());
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets){
+ int fabInset=0;
+ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
+ fabInset=insets.getSystemWindowInsetBottom();
+ }
+ ((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
+ super.onApplyWindowInsets(insets);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
+ inflater.inflate(R.menu.settings_filter_words, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ enterSelectionMode(item.getItemId()==R.id.select_all);
+ return true;
+ }
+
+ @Override
+ public boolean wantsLightStatusBar(){
+ if(actionMode!=null)
+ return UiUtils.isDarkTheme();
+ return super.wantsLightStatusBar();
+ }
+
+ private void onFabClick(){
+ showAlertForWord(null);
+ }
+
+ private void showAlertForWord(FilterKeyword word){
+ AlertDialog.Builder bldr=new M3AlertDialogBuilder(getActivity())
+ .setHelpText(R.string.filter_add_word_help)
+ .setTitle(word==null ? R.string.add_muted_word : R.string.edit_muted_word)
+ .setNegativeButton(R.string.cancel, null);
+
+ FloatingHintEditTextLayout editWrap=(FloatingHintEditTextLayout) bldr.getContext().getSystemService(LayoutInflater.class).inflate(R.layout.floating_hint_edit_text, null);
+ EditText edit=editWrap.findViewById(R.id.edit);
+ edit.setHint(R.string.filter_word_or_phrase);
+ edit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
+ editWrap.updateHint();
+ bldr.setView(editWrap)
+ .setPositiveButton(word==null ? R.string.add : R.string.save, null);
+
+ if(word!=null){
+ edit.setText(word.keyword);
+ bldr.setNeutralButton(R.string.delete, null);
+ }
+ AlertDialog alert=bldr.show();
+ if(word!=null){
+ Button deleteBtn=alert.getButton(AlertDialog.BUTTON_NEUTRAL);
+ deleteBtn.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
+ deleteBtn.setOnClickListener(v->confirmDeleteWords(Collections.singletonList(word), alert::dismiss));
+ }
+ Button saveBtn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
+ saveBtn.setEnabled(false);
+ saveBtn.setOnClickListener(v->{
+ String input=edit.getText().toString();
+ for(ListItem item:data){
+ if(item.parentObject.keyword.equalsIgnoreCase(input)){
+ editWrap.setErrorState(getString(R.string.filter_word_already_in_list));
+ return;
+ }
+ }
+ if(word==null){
+ FilterKeyword w=new FilterKeyword();
+ w.wholeWord=true;
+ w.keyword=input;
+ ListItem item=new ListItem<>(w.keyword, null, null, w);
+ item.isEnabled=true;
+ item.onClick=()->onWordClick(item);
+ data.add(item);
+ itemsAdapter.notifyItemInserted(data.size()-1);
+ }else{
+ word.keyword=input;
+ word.wholeWord=true;
+ for(ListItem item:data){
+ if(item.parentObject==word){
+ rebindItem(item);
+ break;
+ }
+ }
+ }
+ alert.dismiss();
+ });
+ edit.addTextChangedListener(new SimpleTextWatcher(e->saveBtn.setEnabled(e.length()>0)));
+ }
+
+ private void confirmDeleteWords(List words, Runnable onConfirmed){
+ AlertDialog alert=new M3AlertDialogBuilder(getActivity())
+ .setTitle(words.size()==1 ? getString(R.string.settings_delete_filter_word, words.get(0).keyword) : getResources().getQuantityString(R.plurals.settings_delete_x_filter_words, words.size(), words.size()))
+// .setMessage(R.string.settings_delete_filter_confirmation)
+ .setPositiveButton(R.string.delete, (dlg, item)->{
+ if(onConfirmed!=null)
+ onConfirmed.run();
+ removeWords(words);
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
+ }
+
+ private void removeWords(List words){
+ ArrayList indexes=new ArrayList<>();
+ for(int i=0;ii.parentObject).collect(Collectors.toList()), ()->leaveSelectionMode(false));
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode){
+ leaveSelectionMode(true);
+ ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
+ anim.setEvaluator(new IntEvaluator(){
+ @Override
+ public Integer evaluate(float fraction, Integer startValue, Integer endValue){
+ return UiUtils.alphaBlendColors(startValue, endValue, fraction);
+ }
+ });
+ anim.addListener(new AnimatorListenerAdapter(){
+ @Override
+ public void onAnimationEnd(Animator animation){
+ getActivity().getWindow().setStatusBarColor(0);
+ }
+ });
+ anim.start();
+ ((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
+ }
+ });
+
+ selectedItems.clear();
+ for(int i=0;i item=data.get(i);
+ CheckableListItem newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
+ newItem.isEnabled=true;
+ newItem.onClick=()->onSelectionModeWordClick(newItem);
+ newItem.parentObject=item.parentObject;
+ if(selectAll)
+ selectedItems.add(newItem);
+ data.set(i, newItem);
+ }
+ itemsAdapter.notifyItemRangeChanged(0, data.size());
+ updateActionModeTitle();
+ }
+
+ private void leaveSelectionMode(boolean fromActionMode){
+ if(actionMode==null)
+ return;
+ ActionMode actionMode=this.actionMode;
+ this.actionMode=null;
+ if(!fromActionMode)
+ actionMode.finish();
+ V.setVisibilityAnimated(fab, View.VISIBLE);
+ selectedItems.clear();
+
+ for(int i=0;i item=data.get(i);
+ ListItem newItem=new ListItem<>(item.title, null, null);
+ newItem.isEnabled=true;
+ newItem.onClick=()->onWordClick(newItem);
+ newItem.parentObject=item.parentObject;
+ data.set(i, newItem);
+ }
+ itemsAdapter.notifyItemRangeChanged(0, data.size());
+ }
+
+ private void updateActionModeTitle(){
+ actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedItems.size(), selectedItems.size()));
+ deleteItem.setEnabled(!selectedItems.isEmpty());
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java
new file mode 100644
index 00000000..f767e8d7
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java
@@ -0,0 +1,82 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.joinmastodon.android.BuildConfig;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.MastodonAPIController;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.utils.UiUtils;
+
+import java.util.List;
+
+import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.imageloader.ImageCache;
+import me.grishka.appkit.utils.MergeRecyclerAdapter;
+import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
+import me.grishka.appkit.utils.V;
+
+public class SettingsAboutAppFragment extends BaseSettingsFragment{
+ private ListItem mediaCacheItem;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(getString(R.string.about_app, getString(R.string.app_name)));
+ AccountSession s=AccountSessionManager.get(accountID);
+ onDataLoaded(List.of(
+ new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
+ new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
+ new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
+ new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
+ mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
+ ));
+
+ updateMediaCacheItem();
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
+ adapter.addAdapter(super.getAdapter());
+
+ TextView versionInfo=new TextView(getActivity());
+ versionInfo.setSingleLine();
+ versionInfo.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(32)));
+ versionInfo.setTextAppearance(R.style.m3_label_medium);
+ versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
+ versionInfo.setGravity(Gravity.CENTER);
+ versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
+ adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
+
+ return adapter;
+ }
+
+ private void onClearMediaCacheClick(){
+ MastodonAPIController.runInBackground(()->{
+ Activity activity=getActivity();
+ ImageCache.getInstance(getActivity()).clear();
+ activity.runOnUiThread(()->{
+ Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
+ updateMediaCacheItem();
+ });
+ });
+ }
+
+ private void updateMediaCacheItem(){
+ long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
+ mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
+ mediaCacheItem.isEnabled=size>0;
+ rebindItem(mediaCacheItem);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java
new file mode 100644
index 00000000..28181860
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsBehaviorFragment.java
@@ -0,0 +1,83 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.os.Bundle;
+
+import org.joinmastodon.android.GlobalUserPreferences;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.model.Preferences;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
+
+import java.util.List;
+import java.util.Locale;
+
+public class SettingsBehaviorFragment extends BaseSettingsFragment{
+ private ListItem languageItem;
+ private CheckableListItem altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
+ private Locale postLanguage;
+ private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_behavior);
+
+ AccountSession s=AccountSessionManager.get(accountID);
+ if(s.preferences!=null && s.preferences.postingDefaultLanguage!=null){
+ postLanguage=Locale.forLanguageTag(s.preferences.postingDefaultLanguage);
+ }
+
+ onDataLoaded(List.of(
+ languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
+ altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, ()->toggleCheckableItem(altTextItem)),
+ playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, ()->toggleCheckableItem(playGifsItem)),
+ customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, ()->toggleCheckableItem(customTabsItem)),
+ confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, ()->toggleCheckableItem(confirmUnfollowItem)),
+ confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, ()->toggleCheckableItem(confirmBoostItem)),
+ confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, ()->toggleCheckableItem(confirmDeleteItem))
+ ));
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ private void onDefaultLanguageClick(){
+ ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
+ new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.default_post_language)
+ .setView(vc.getView())
+ .setPositiveButton(R.string.ok, (dlg, which)->{
+ ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
+ if(!opt.locale.equals(postLanguage)){
+ newPostLanguage=opt;
+ languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
+ rebindItem(languageItem);
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ @Override
+ protected void onHidden(){
+ super.onHidden();
+ GlobalUserPreferences.playGifs=playGifsItem.checked;
+ GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
+ GlobalUserPreferences.altTextReminders=altTextItem.checked;
+ GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
+ GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
+ GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
+ GlobalUserPreferences.save();
+ if(newPostLanguage!=null){
+ AccountSession s=AccountSessionManager.get(accountID);
+ if(s.preferences==null)
+ s.preferences=new Preferences();
+ s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
+ s.savePreferencesLater();
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java
new file mode 100644
index 00000000..9176908d
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDebugFragment.java
@@ -0,0 +1,63 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.os.Bundle;
+
+import org.joinmastodon.android.api.session.AccountActivationInfo;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.fragments.HomeFragment;
+import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.updater.GithubSelfUpdater;
+
+import java.util.List;
+
+import me.grishka.appkit.Nav;
+
+public class SettingsDebugFragment extends BaseSettingsFragment{
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle("Debug settings");
+ ListItem selfUpdateItem, resetUpdateItem;
+ onDataLoaded(List.of(
+ new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
+ selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
+ resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick)
+ ));
+ if(!GithubSelfUpdater.needSelfUpdating()){
+ resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
+ selfUpdateItem.subtitle="Self-updater is unavailable in this build flavor";
+ }
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ private void onTestEmailConfirmClick(){
+ AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
+ sess.activated=false;
+ sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putBoolean("debug", true);
+ Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
+ }
+
+ private void onForceSelfUpdateClick(){
+ GithubSelfUpdater.forceUpdate=true;
+ GithubSelfUpdater.getInstance().maybeCheckForUpdates();
+ restartUI();
+ }
+
+ private void onResetUpdaterClick(){
+ GithubSelfUpdater.getInstance().reset();
+ restartUI();
+ }
+
+ private void restartUI(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ Nav.goClearingStack(getActivity(), HomeFragment.class, args);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDisplayFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDisplayFragment.java
new file mode 100644
index 00000000..27571a80
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsDisplayFragment.java
@@ -0,0 +1,152 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import org.joinmastodon.android.E;
+import org.joinmastodon.android.GlobalUserPreferences;
+import org.joinmastodon.android.MastodonApp;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountLocalPreferences;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+import me.grishka.appkit.FragmentStackActivity;
+
+public class SettingsDisplayFragment extends BaseSettingsFragment{
+ private ImageView themeTransitionWindowView;
+ private ListItem themeItem;
+ private CheckableListItem showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_display);
+ AccountSession s=AccountSessionManager.get(accountID);
+ AccountLocalPreferences lp=s.getLocalPreferences();
+ onDataLoaded(List.of(
+ themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
+ showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, ()->toggleCheckableItem(showCWsItem)),
+ hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, ()->toggleCheckableItem(hideSensitiveMediaItem)),
+ interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, ()->toggleCheckableItem(interactionCountsItem)),
+ emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
+ ));
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ public void onAttach(Activity activity){
+ super.onAttach(activity);
+ if(themeTransitionWindowView!=null){
+ // Activity has finished recreating. Remove the overlay.
+ activity.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
+ themeTransitionWindowView=null;
+ }
+ }
+
+ @Override
+ protected void onHidden(){
+ super.onHidden();
+ AccountSession s=AccountSessionManager.get(accountID);
+ AccountLocalPreferences lp=s.getLocalPreferences();
+ lp.showCWs=showCWsItem.checked;
+ lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
+ lp.showInteractionCounts=interactionCountsItem.checked;
+ lp.customEmojiInNames=emojiInNamesItem.checked;
+ lp.save();
+ E.post(new StatusDisplaySettingsChangedEvent(accountID));
+ }
+
+ private int getAppearanceValue(){
+ return switch(GlobalUserPreferences.theme){
+ case AUTO -> R.string.theme_auto;
+ case LIGHT -> R.string.theme_light;
+ case DARK -> R.string.theme_dark;
+ };
+ }
+
+ private void onAppearanceClick(){
+ int selected=switch(GlobalUserPreferences.theme){
+ case LIGHT -> 0;
+ case DARK -> 1;
+ case AUTO -> 2;
+ };
+ int[] newSelected={selected};
+ new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.settings_theme)
+ .setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
+ selected, (dlg, item)->newSelected[0]=item)
+ .setPositiveButton(R.string.ok, (dlg, item)->{
+ GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
+ case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
+ case 1 -> GlobalUserPreferences.ThemePreference.DARK;
+ case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
+ default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
+ };
+ if(pref!=GlobalUserPreferences.theme){
+ GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
+ GlobalUserPreferences.theme=pref;
+ GlobalUserPreferences.save();
+ themeItem.subtitleRes=getAppearanceValue();
+ rebindItem(themeItem);
+ maybeApplyNewThemeRightNow(prev);
+ }
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prev){
+ boolean isCurrentDark=prev==GlobalUserPreferences.ThemePreference.DARK ||
+ (prev==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
+ boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
+ (GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
+ if(isCurrentDark!=isNewDark){
+ restartActivityToApplyNewTheme();
+ }
+ }
+
+ private void restartActivityToApplyNewTheme(){
+ // Calling activity.recreate() causes a black screen for like half a second.
+ // So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
+ // As a bonus, we can fade it out to make it even smoother.
+ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N && Build.VERSION.SDK_INT{
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_filters);
+ loadData();
+ E.register(this);
+ }
+
+ @Override
+ public void onDestroy(){
+ super.onDestroy();
+ E.unregister(this);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){
+ new GetFilters()
+ .setCallback(new SimpleCallback<>(this){
+ @Override
+ public void onSuccess(List result){
+ onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
+ }
+ })
+ .exec(accountID);
+ }
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
+ adapter.addAdapter(super.getAdapter());
+ adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
+ new ListItem(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
+ )));
+ return adapter;
+ }
+
+ private void onFilterClick(ListItem filter){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putParcelable("filter", Parcels.wrap(filter.parentObject));
+ Nav.go(getActivity(), EditFilterFragment.class, args);
+ }
+
+ private void onAddFilterClick(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ Nav.go(getActivity(), EditFilterFragment.class, args);
+ }
+
+ private ListItem makeListItem(Filter f){
+ ListItem item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
+ item.onClick=()->onFilterClick(item);
+ item.isEnabled=true;
+ return item;
+ }
+
+ @Subscribe
+ public void onFilterDeleted(SettingsFilterDeletedEvent ev){
+ if(!ev.accountID.equals(accountID))
+ return;
+ for(int i=0;i item:data){
+ if(item.parentObject.id.equals(ev.filter.id)){
+ item.parentObject=ev.filter;
+ item.title=ev.filter.title;
+ item.subtitle=getString(ev.filter.isActive() ? R.string.filter_active : R.string.filter_inactive);
+ rebindItem(item);
+ return;
+ }
+ }
+ data.add(makeListItem(ev.filter));
+ itemsAdapter.notifyItemInserted(data.size()-1);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java
new file mode 100644
index 00000000..83985e79
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java
@@ -0,0 +1,213 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.squareup.otto.Subscribe;
+
+import org.joinmastodon.android.BuildConfig;
+import org.joinmastodon.android.E;
+import org.joinmastodon.android.MainActivity;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.updater.GithubSelfUpdater;
+
+import java.util.List;
+
+import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.Nav;
+import me.grishka.appkit.utils.MergeRecyclerAdapter;
+
+public class SettingsMainFragment extends BaseSettingsFragment{
+ private boolean loggedOut;
+ private HideableSingleViewRecyclerAdapter bannerAdapter;
+ private Button updateButton1, updateButton2;
+ private TextView updateText;
+ private Runnable updateDownloadProgressUpdater=new Runnable(){
+ @Override
+ public void run(){
+ GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
+ if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
+ updateButton1.setText(getString(R.string.downloading_update, Math.round(GithubSelfUpdater.getInstance().getDownloadProgress()*100f)));
+ list.postDelayed(this, 250);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings);
+ setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
+ onDataLoaded(List.of(
+ new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
+ new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
+ new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
+ new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
+ new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_lock_24px, this::onPrivacyClick, 0, true),
+ new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
+ new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
+ new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
+ ));
+
+ if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
+ data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
+ }
+
+ AccountSessionManager.get(accountID).reloadPreferences(null);
+ E.register(this);
+ }
+
+ @Override
+ public void onDestroy(){
+ super.onDestroy();
+ E.unregister(this);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ protected void onUpdateToolbar(){
+ super.onUpdateToolbar();
+ UiUtils.setToolbarWithSubtitleAppearance(getToolbar());
+ }
+
+ @Override
+ protected void onHidden(){
+ super.onHidden();
+ if(!loggedOut)
+ AccountSessionManager.get(accountID).savePreferencesIfPending();
+ }
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
+ updateText=banner.findViewById(R.id.text);
+ TextView bannerTitle=banner.findViewById(R.id.title);
+ ImageView bannerIcon=banner.findViewById(R.id.icon);
+ updateButton1=banner.findViewById(R.id.button);
+ updateButton2=banner.findViewById(R.id.button2);
+ bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
+ bannerAdapter.setVisible(false);
+ updateButton1.setOnClickListener(this::onUpdateButtonClick);
+ updateButton2.setOnClickListener(this::onUpdateButtonClick);
+
+ bannerTitle.setText(R.string.app_update_ready);
+ bannerIcon.setImageResource(R.drawable.ic_apk_install_24px);
+
+ MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
+ adapter.addAdapter(bannerAdapter);
+ adapter.addAdapter(super.getAdapter());
+ return adapter;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState){
+ super.onViewCreated(view, savedInstanceState);
+ if(GithubSelfUpdater.needSelfUpdating()){
+ updateUpdateBanner();
+ }
+ }
+
+ private Bundle makeFragmentArgs(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ return args;
+ }
+
+ private void onBehaviorClick(){
+ Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
+ }
+
+ private void onDisplayClick(){
+ Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
+ }
+
+ private void onFiltersClick(){
+ Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
+ }
+
+ private void onNotificationsClick(){
+ Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
+ }
+
+ private void onPrivacyClick(){
+
+ }
+
+ private void onServerClick(){
+ Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
+ }
+
+ private void onAboutClick(){
+ Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
+ }
+
+ private void onLogOutClick(){
+ AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
+ new M3AlertDialogBuilder(getActivity())
+ .setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
+ .setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
+ loggedOut=true;
+ getActivity().finish();
+ Intent intent=new Intent(getActivity(), MainActivity.class);
+ startActivity(intent);
+ }))
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ @Subscribe
+ public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
+ updateUpdateBanner();
+ }
+
+ private void updateUpdateBanner(){
+ GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
+ if(state==GithubSelfUpdater.UpdateState.NO_UPDATE || state==GithubSelfUpdater.UpdateState.CHECKING){
+ bannerAdapter.setVisible(false);
+ }else{
+ bannerAdapter.setVisible(true);
+ updateText.setText(getString(R.string.app_update_version, GithubSelfUpdater.getInstance().getUpdateInfo().version));
+ if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
+ updateButton2.setVisibility(View.GONE);
+ updateButton1.setEnabled(true);
+ updateButton1.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), GithubSelfUpdater.getInstance().getUpdateInfo().size, true)));
+ }else if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
+ updateButton2.setVisibility(View.VISIBLE);
+ updateButton2.setText(R.string.cancel);
+ updateButton1.setEnabled(false);
+ list.removeCallbacks(updateDownloadProgressUpdater);
+ updateDownloadProgressUpdater.run();
+ }else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
+ updateButton2.setVisibility(View.GONE);
+ updateButton1.setEnabled(true);
+ updateButton1.setText(R.string.install_update);
+ }
+ }
+ }
+
+ private void onUpdateButtonClick(View v){
+ if(v.getId()==R.id.button){
+ GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
+ if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
+ GithubSelfUpdater.getInstance().downloadUpdate();
+ }else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
+ GithubSelfUpdater.getInstance().installUpdate(getActivity());
+ }
+ }else if(v.getId()==R.id.button2){
+ GithubSelfUpdater.getInstance().cancelDownload();
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java
new file mode 100644
index 00000000..b8a8d506
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsNotificationsFragment.java
@@ -0,0 +1,286 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.app.AlertDialog;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.PushSubscriptionManager;
+import org.joinmastodon.android.api.session.AccountSession;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.model.PushSubscription;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
+import org.joinmastodon.android.ui.utils.UiUtils;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.utils.MergeRecyclerAdapter;
+
+public class SettingsNotificationsFragment extends BaseSettingsFragment{
+ private PushSubscription pushSubscription;
+ private CheckableListItem pauseItem;
+ private ListItem policyItem;
+ private MergeRecyclerAdapter mergeAdapter;
+
+ private HideableSingleViewRecyclerAdapter bannerAdapter;
+ private ImageView bannerIcon;
+ private TextView bannerText;
+ private Button bannerButton;
+
+ private CheckableListItem mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
+ private List> typeItems;
+ private boolean needUpdateNotificationSettings;
+ private boolean notificationsAllowed=true;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.settings_notifications);
+
+ getPushSubscription();
+
+ onDataLoaded(List.of(
+ pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
+ policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick),
+
+ mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, ()->toggleCheckableItem(mentionsItem)),
+ boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, ()->toggleCheckableItem(boostsItem)),
+ favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, ()->toggleCheckableItem(favoritesItem)),
+ followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, ()->toggleCheckableItem(followersItem)),
+ pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, ()->toggleCheckableItem(pollsItem))
+ ));
+
+ typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
+ pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
+ updatePolicyItem(null);
+ updatePauseItem();
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ protected void onHidden(){
+ super.onHidden();
+ PushSubscription ps=getPushSubscription();
+ needUpdateNotificationSettings|=mentionsItem.checked!=ps.alerts.mention
+ || boostsItem.checked!=ps.alerts.reblog
+ || favoritesItem.checked!=ps.alerts.favourite
+ || followersItem.checked!=ps.alerts.follow
+ || pollsItem.checked!=ps.alerts.poll;
+ if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
+ ps.alerts.mention=mentionsItem.checked;
+ ps.alerts.reblog=boostsItem.checked;
+ ps.alerts.favourite=favoritesItem.checked;
+ ps.alerts.follow=followersItem.checked;
+ ps.alerts.poll=pollsItem.checked;
+ AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
+ }
+ }
+
+ @Override
+ protected void onShown(){
+ super.onShown();
+ boolean allowed=areNotificationsAllowed();
+ PushSubscription ps=getPushSubscription();
+ if(allowed!=notificationsAllowed){
+ notificationsAllowed=allowed;
+ updateBanner();
+ pauseItem.isEnabled=allowed;
+ policyItem.isEnabled=allowed;
+ rebindItem(pauseItem);
+ rebindItem(policyItem);
+ for(CheckableListItem item:typeItems){
+ item.isEnabled=allowed && ps.policy!=PushSubscription.Policy.NONE;
+ rebindItem(item);
+ }
+ }
+ }
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
+ bannerText=banner.findViewById(R.id.text);
+ bannerIcon=banner.findViewById(R.id.icon);
+ bannerButton=banner.findViewById(R.id.button);
+ bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
+ bannerAdapter.setVisible(false);
+ banner.findViewById(R.id.button2).setVisibility(View.GONE);
+ banner.findViewById(R.id.title).setVisibility(View.GONE);
+
+ mergeAdapter=new MergeRecyclerAdapter();
+ mergeAdapter.addAdapter(bannerAdapter);
+ mergeAdapter.addAdapter(super.getAdapter());
+ return mergeAdapter;
+ }
+
+ @Override
+ protected int indexOfItemsAdapter(){
+ return mergeAdapter.getPositionForAdapter(itemsAdapter);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState){
+ super.onViewCreated(view, savedInstanceState);
+ updateBanner();
+ }
+
+ private boolean areNotificationsAllowed(){
+ return Build.VERSION.SDK_INTSystem.currentTimeMillis() && fromSwitch){
+ resumePausedNotifications();
+ return;
+ }
+ int[] durationOptions={
+ 1800,
+ 3600,
+ 6*3600,
+ 12*3600,
+ 24*3600,
+ 3*24*3600,
+ 7*24*3600
+ };
+ int[] selectedOption={0};
+ AlertDialog alert=new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.pause_all_notifications_title)
+ .setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
+ .setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
+ if(selectedOption[0]==0){
+ ((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
+ }
+ selectedOption[0]=durationOptions[item];
+ })
+ .setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ alert.setOnDismissListener(dialog->updatePauseItem());
+ alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ }
+
+ private void onNotificationsPolicyClick(){
+ String[] items=Stream.of(
+ R.string.notifications_policy_anyone,
+ R.string.notifications_policy_followed,
+ R.string.notifications_policy_follower,
+ R.string.notifications_policy_no_one
+ ).map(this::getString).toArray(String[]::new);
+ int[] selectedItem={getPushSubscription().policy.ordinal()};
+ new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.settings_notifications_policy)
+ .setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
+ .setPositiveButton(R.string.ok, (dlg, which)->{
+ PushSubscription.Policy prevValue=getPushSubscription().policy;
+ PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
+ if(prevValue==newValue)
+ return;
+ getPushSubscription().policy=newValue;
+ updatePolicyItem(prevValue);
+ needUpdateNotificationSettings=true;
+ })
+ .setNegativeButton(R.string.cancel, null)
+ .show();
+ }
+
+ private void updatePolicyItem(PushSubscription.Policy prevValue){
+ policyItem.subtitleRes=switch(getPushSubscription().policy){
+ case ALL -> R.string.notifications_policy_anyone;
+ case FOLLOWED -> R.string.notifications_policy_followed;
+ case FOLLOWER -> R.string.notifications_policy_follower;
+ case NONE -> R.string.notifications_policy_no_one;
+ };
+ rebindItem(policyItem);
+ if(pushSubscription.policy==PushSubscription.Policy.NONE || prevValue==PushSubscription.Policy.NONE){
+ for(CheckableListItem item:typeItems){
+ item.checked=item.isEnabled=prevValue==PushSubscription.Policy.NONE;
+ rebindItem(item);
+ }
+ }
+ }
+
+ private void updatePauseItem(){
+ long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
+ if(timeopenSystemNotificationSettings());
+ }else if(pauseTime>System.currentTimeMillis()){
+ bannerAdapter.setVisible(true);
+ bannerIcon.setImageResource(R.drawable.ic_notifications_paused_24px);
+ bannerText.setText(getString(R.string.pause_notifications_banner, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(pauseTime), false)));
+ bannerButton.setText(R.string.resume_notifications_now);
+ bannerButton.setOnClickListener(v->resumePausedNotifications());
+ }else{
+ bannerAdapter.setVisible(false);
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerAboutFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerAboutFragment.java
new file mode 100644
index 00000000..c2fdde75
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerAboutFragment.java
@@ -0,0 +1,238 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.MastodonAPIController;
+import org.joinmastodon.android.api.requests.instance.GetInstanceExtendedDescription;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.model.Instance;
+import org.joinmastodon.android.model.viewmodel.AccountViewModel;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.OutlineProviders;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
+import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
+import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
+import org.joinmastodon.android.utils.ViewImageLoaderHolderTarget;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import me.grishka.appkit.api.SimpleCallback;
+import me.grishka.appkit.fragments.LoaderFragment;
+import me.grishka.appkit.imageloader.ViewImageLoader;
+import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
+import me.grishka.appkit.utils.V;
+
+public class SettingsServerAboutFragment extends LoaderFragment{
+ private String accountID;
+ private Instance instance;
+
+ private WebView webView;
+ private LinearLayout scrollingLayout;
+ public ScrollView scroller;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ accountID=getArguments().getString("account");
+ instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
+ loadData();
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
+ webView=new WebView(getActivity());
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.setWebViewClient(new WebViewClient(){
+ @Override
+ public void onPageFinished(WebView view, String url){
+ dataLoaded();
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url){
+ Uri uri=Uri.parse(url);
+ if(uri.getScheme().equals("http") || uri.getScheme().equals("https")){
+ UiUtils.launchWebBrowser(getActivity(), url);
+ }else{
+ Intent intent=new Intent(Intent.ACTION_VIEW, uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try{
+ startActivity(intent);
+ }catch(ActivityNotFoundException x){
+ Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
+ }
+ }
+ return true;
+ }
+ });
+
+ scrollingLayout=new LinearLayout(getActivity());
+ scrollingLayout.setOrientation(LinearLayout.VERTICAL);
+ scroller=new ScrollView(getActivity());
+ scroller.setNestedScrollingEnabled(true);
+ scroller.setClipToPadding(false);
+ scroller.addView(scrollingLayout);
+
+ FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
+ banner.setAspectRatio(1.914893617f);
+ banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
+ banner.setClipToOutline(true);
+ ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
+ LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ blp.bottomMargin=V.dp(24);
+ scrollingLayout.addView(banner, blp);
+
+ boolean needDivider=false;
+ if(instance.contactAccount!=null){
+ needDivider=true;
+ TextView heading=new TextView(getActivity());
+ heading.setTextAppearance(R.style.m3_title_small);
+ heading.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant));
+ heading.setSingleLine();
+ heading.setText(R.string.server_administrator);
+ heading.setGravity(Gravity.CENTER_VERTICAL);
+ LinearLayout.LayoutParams hlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20));
+ hlp.bottomMargin=V.dp(4);
+ hlp.leftMargin=hlp.rightMargin=V.dp(16);
+ scrollingLayout.addView(heading, hlp);
+
+ AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
+ AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
+ holder.bind(model);
+ holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
+ holder.itemView.setOnClickListener(v->holder.onClick());
+ scrollingLayout.addView(holder.itemView);
+ ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, 0), null, model.avaRequest, false);
+ for(int i=0;i item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, ()->{});
+ holder.bind(item);
+ holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
+ holder.itemView.setOnClickListener(v->openAdminEmail());
+ scrollingLayout.addView(holder.itemView);
+ }
+ if(needDivider){
+ View divider=new View(getActivity());
+ divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
+ LinearLayout.LayoutParams dlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1));
+ dlp.leftMargin=dlp.rightMargin=V.dp(16);
+ scrollingLayout.addView(divider, dlp);
+ }
+ scrollingLayout.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ return scroller;
+ }
+
+ @Override
+ protected void doLoadData(){
+ new GetInstanceExtendedDescription()
+ .setCallback(new SimpleCallback<>(this){
+ @Override
+ public void onSuccess(GetInstanceExtendedDescription.Response result){
+ MastodonAPIController.runInBackground(()->{
+ Activity activity=getActivity();
+ if(activity==null)
+ return;
+ String template;
+ try(BufferedReader reader=new BufferedReader(new InputStreamReader(getActivity().getAssets().open("server_about_template.htm")))){
+ StringBuilder sb=new StringBuilder();
+ String line;
+ while((line=reader.readLine())!=null){
+ sb.append(line);
+ sb.append('\n');
+ }
+ template=sb.toString();
+ }catch(IOException x){
+ throw new RuntimeException(x);
+ }
+
+ HashMap templateParams=new HashMap<>();
+ templateParams.put("content", result.content);
+ templateParams.put("colorSurface", getThemeColorAsCss(R.attr.colorM3Surface, 1));
+ templateParams.put("colorOnSurface", getThemeColorAsCss(R.attr.colorM3OnSurface, 1));
+ templateParams.put("colorPrimary", getThemeColorAsCss(R.attr.colorM3Primary, 1));
+ templateParams.put("colorPrimaryTransparent", getThemeColorAsCss(R.attr.colorM3Primary, 0.2f));
+ for(Map.Entry param:templateParams.entrySet()){
+ template=template.replace("{{"+param.getKey()+"}}", param.getValue());
+ }
+
+ final String html=template;
+ activity.runOnUiThread(()->{
+ webView.loadDataWithBaseURL(null, html, "text/html; charset=utf-8", null, null);
+ });
+ });
+ }
+ })
+ .exec(accountID);
+ }
+
+ @Override
+ public void onRefresh(){}
+
+ private void openAdminEmail(){
+ Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try{
+ startActivity(intent);
+ }catch(ActivityNotFoundException x){
+ Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets){
+ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
+ scroller.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
+ }else{
+ scroller.setPadding(0, 0, 0, 0);
+ }
+ super.onApplyWindowInsets(insets);
+ }
+
+ private String getThemeColorAsCss(int attr, float alpha){
+ int color=UiUtils.getThemeColor(getActivity(), attr);
+ if(alpha==1f){
+ return String.format(Locale.US, "#%06X", color & 0xFFFFFF);
+ }else{
+ int r=(color >> 16) & 0xFF;
+ int g=(color >> 8) & 0xFF;
+ int b=color & 0xFF;
+ return "rgba("+r+","+g+","+b+","+alpha+")";
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java
new file mode 100644
index 00000000..4ff696aa
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerFragment.java
@@ -0,0 +1,185 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.app.Fragment;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.model.Instance;
+import org.joinmastodon.android.ui.SimpleViewHolder;
+import org.joinmastodon.android.ui.tabs.TabLayout;
+import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager2.widget.ViewPager2;
+import me.grishka.appkit.fragments.AppKitFragment;
+import me.grishka.appkit.utils.V;
+
+public class SettingsServerFragment extends AppKitFragment{
+ private String accountID;
+ private Instance instance;
+ private TabLayout tabBar;
+ private TabLayoutMediator tabLayoutMediator;
+ private ViewPager2 pager;
+ private FrameLayout[] tabViews;
+ private View contentView;
+ private WindowInsets childInsets;
+
+ private SettingsServerAboutFragment aboutFragment;
+ private SettingsServerRulesFragment rulesFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ accountID=getArguments().getString("account");
+ setTitle(AccountSessionManager.get(accountID).domain);
+
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putBoolean("__is_tab", true);
+ aboutFragment=new SettingsServerAboutFragment();
+ aboutFragment.setArguments(args);
+ rulesFragment=new SettingsServerRulesFragment();
+ rulesFragment.setArguments(args);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
+ View view=inflater.inflate(R.layout.fragment_settings_server, container, false);
+
+ TextView realTitle=view.findViewById(R.id.real_title);
+ realTitle.setText(getTitle());
+ realTitle.setSelected(true);
+
+ pager=view.findViewById(R.id.pager);
+ pager.setAdapter(new ServerPagerAdapter());
+
+ FrameLayout sizeWrapper=new FrameLayout(getActivity()){
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
+ pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ };
+
+ tabViews=new FrameLayout[2];
+ for(int i=0;i R.id.server_about;
+ case 1 -> R.id.server_rules;
+ default -> throw new IllegalStateException("Unexpected value: "+i);
+ });
+ tabView.setVisibility(View.GONE);
+ sizeWrapper.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
+ tabViews[i]=tabView;
+ }
+
+ tabBar=view.findViewById(R.id.tabbar);
+ tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
+ tabBar.setTabTextSize(V.dp(16));
+ tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){
+ case 0 -> R.string.about_server;
+ case 1 -> R.string.server_rules;
+ default -> throw new IllegalStateException("Unexpected value: "+position);
+ }));
+ tabLayoutMediator.attach();
+
+ NestedRecyclerScrollView scrollView=view.findViewById(R.id.scroller);
+ scrollView.setScrollableChildSupplier(()->switch(pager.getCurrentItem()){
+ case 0 -> aboutFragment.scroller;
+ case 1 -> rulesFragment.getList();
+ default -> throw new IllegalStateException("Unexpected value: "+pager.getCurrentItem());
+ });
+
+ return contentView=view;
+ }
+
+ @Override
+ protected void onUpdateToolbar(){
+ super.onUpdateToolbar();
+ getToolbar().setTitle(null);
+ }
+
+ private Fragment getFragmentForPage(int page){
+ return switch(page){
+ case 0 -> aboutFragment;
+ case 1 -> rulesFragment;
+ default -> throw new IllegalStateException();
+ };
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets){
+ if(contentView!=null){
+ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
+ int insetBottom=insets.getSystemWindowInsetBottom();
+ childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
+ applyChildWindowInsets();
+ insets=insets.inset(0, 0, 0, insetBottom);
+ }
+ }
+ super.onApplyWindowInsets(insets);
+ }
+
+ private void applyChildWindowInsets(){
+ if(aboutFragment!=null && aboutFragment.isAdded() && childInsets!=null){
+ aboutFragment.onApplyWindowInsets(childInsets);
+ rulesFragment.onApplyWindowInsets(childInsets);
+ }
+ }
+
+ private class ServerPagerAdapter extends RecyclerView.Adapter{
+ @NonNull
+ @Override
+ public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
+ FrameLayout view=tabViews[viewType];
+ ((ViewGroup)view.getParent()).removeView(view);
+ view.setVisibility(View.VISIBLE);
+ view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ return new SimpleViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
+ Fragment fragment=getFragmentForPage(position);
+ if(!fragment.isAdded()){
+ getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
+ holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
+ @Override
+ public boolean onPreDraw(){
+ getChildFragmentManager().executePendingTransactions();
+ if(fragment.isAdded()){
+ holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
+ applyChildWindowInsets();
+ }
+ return true;
+ }
+ });
+ }
+ }
+
+ @Override
+ public int getItemCount(){
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position){
+ return position;
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerRulesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerRulesFragment.java
new file mode 100644
index 00000000..198fca6a
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsServerRulesFragment.java
@@ -0,0 +1,47 @@
+package org.joinmastodon.android.fragments.settings;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSessionManager;
+import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
+import org.joinmastodon.android.model.Instance;
+import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public class SettingsServerRulesFragment extends MastodonRecyclerFragment{
+ private String accountID;
+
+ public SettingsServerRulesFragment(){
+ super(20);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState){
+ super.onCreate(savedInstanceState);
+ accountID=getArguments().getString("account");
+ Instance instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
+ onDataLoaded(instance.rules);
+ setRefreshEnabled(false);
+ }
+
+ @Override
+ protected void doLoadData(int offset, int count){}
+
+ @Override
+ protected RecyclerView.Adapter> getAdapter(){
+ return new InstanceRulesAdapter(data);
+ }
+
+ @Override
+ protected View onCreateFooterView(LayoutInflater inflater){
+ return inflater.inflate(R.layout.load_more_with_end_mark, null);
+ }
+
+ public RecyclerView getList(){
+ return list;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java b/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java
index 8a629a29..a768e334 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java
@@ -1,79 +1,56 @@
package org.joinmastodon.android.model;
-import android.text.TextUtils;
-
-import com.google.gson.annotations.SerializedName;
-
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
+import org.parceler.Parcel;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
-import java.util.regex.Pattern;
+@Parcel
public class Filter extends BaseModel{
@RequiredField
public String id;
+
@RequiredField
- public String phrase;
- public transient EnumSet context=EnumSet.noneOf(FilterContext.class);
+ public String title;
+
+ @RequiredField
+ public EnumSet context;
+
public Instant expiresAt;
- public boolean irreversible;
- public boolean wholeWord;
+ public FilterAction filterAction;
- @SerializedName("context")
- private List _context;
+ @RequiredField
+ public List keywords;
- private transient Pattern pattern;
+ @RequiredField
+ public List statuses;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
- if(_context==null)
- throw new ObjectValidationException();
- for(FilterContext c:_context){
- if(c!=null)
- context.add(c);
- }
+ for(FilterKeyword keyword:keywords)
+ keyword.postprocess();
+ for(FilterStatus status:statuses)
+ status.postprocess();
}
- public boolean matches(CharSequence text){
- if(TextUtils.isEmpty(text))
- return false;
- if(pattern==null){
- if(wholeWord)
- pattern=Pattern.compile("\\b"+Pattern.quote(phrase)+"\\b", Pattern.CASE_INSENSITIVE);
- else
- pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
- }
- return pattern.matcher(text).find();
- }
-
- public boolean matches(Status status){
- return matches(status.getContentStatus().getStrippedText());
+ public boolean isActive(){
+ return expiresAt==null || expiresAt.isAfter(Instant.now());
}
@Override
public String toString(){
return "Filter{"+
"id='"+id+'\''+
- ", phrase='"+phrase+'\''+
+ ", title='"+title+'\''+
", context="+context+
", expiresAt="+expiresAt+
- ", irreversible="+irreversible+
- ", wholeWord="+wholeWord+
+ ", filterAction="+filterAction+
+ ", keywords="+keywords+
+ ", statuses="+statuses+
'}';
}
-
- public enum FilterContext{
- @SerializedName("home")
- HOME,
- @SerializedName("notifications")
- NOTIFICATIONS,
- @SerializedName("public")
- PUBLIC,
- @SerializedName("thread")
- THREAD
- }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FilterAction.java b/mastodon/src/main/java/org/joinmastodon/android/model/FilterAction.java
new file mode 100644
index 00000000..1fcdcf7d
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/FilterAction.java
@@ -0,0 +1,10 @@
+package org.joinmastodon.android.model;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum FilterAction{
+ @SerializedName("warn")
+ WARN,
+ @SerializedName("hide")
+ HIDE
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FilterContext.java b/mastodon/src/main/java/org/joinmastodon/android/model/FilterContext.java
new file mode 100644
index 00000000..1019f3fd
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/FilterContext.java
@@ -0,0 +1,31 @@
+package org.joinmastodon.android.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.joinmastodon.android.R;
+
+import androidx.annotation.StringRes;
+
+public enum FilterContext{
+ @SerializedName("home")
+ HOME,
+ @SerializedName("notifications")
+ NOTIFICATIONS,
+ @SerializedName("public")
+ PUBLIC,
+ @SerializedName("thread")
+ THREAD,
+ @SerializedName("account")
+ ACCOUNT;
+
+ @StringRes
+ public int getDisplayNameRes(){
+ return switch(this){
+ case HOME -> R.string.filter_context_home_lists;
+ case NOTIFICATIONS -> R.string.filter_context_notifications;
+ case PUBLIC -> R.string.filter_context_public_timelines;
+ case THREAD -> R.string.filter_context_threads_replies;
+ case ACCOUNT -> R.string.filter_context_profiles;
+ };
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FilterKeyword.java b/mastodon/src/main/java/org/joinmastodon/android/model/FilterKeyword.java
new file mode 100644
index 00000000..3446e459
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/FilterKeyword.java
@@ -0,0 +1,21 @@
+package org.joinmastodon.android.model;
+
+import org.joinmastodon.android.api.AllFieldsAreRequired;
+import org.parceler.Parcel;
+
+@AllFieldsAreRequired
+@Parcel
+public class FilterKeyword extends BaseModel{
+ public String id;
+ public String keyword;
+ public boolean wholeWord;
+
+ @Override
+ public String toString(){
+ return "FilterKeyword{"+
+ "id='"+id+'\''+
+ ", keyword='"+keyword+'\''+
+ ", wholeWord="+wholeWord+
+ '}';
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FilterStatus.java b/mastodon/src/main/java/org/joinmastodon/android/model/FilterStatus.java
new file mode 100644
index 00000000..ae1e4b99
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/FilterStatus.java
@@ -0,0 +1,11 @@
+package org.joinmastodon.android.model;
+
+import org.joinmastodon.android.api.AllFieldsAreRequired;
+import org.parceler.Parcel;
+
+@AllFieldsAreRequired
+@Parcel
+public class FilterStatus extends BaseModel{
+ public String id;
+ public String statusId;
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java b/mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java
new file mode 100644
index 00000000..7b83aa8d
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java
@@ -0,0 +1,69 @@
+package org.joinmastodon.android.model;
+
+import android.text.TextUtils;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.joinmastodon.android.api.ObjectValidationException;
+import org.joinmastodon.android.api.RequiredField;
+
+import java.time.Instant;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class LegacyFilter extends BaseModel{
+ @RequiredField
+ public String id;
+ @RequiredField
+ public String phrase;
+ public transient EnumSet context=EnumSet.noneOf(FilterContext.class);
+ public Instant expiresAt;
+ public boolean irreversible;
+ public boolean wholeWord;
+
+ @SerializedName("context")
+ private List _context;
+
+ private transient Pattern pattern;
+
+ @Override
+ public void postprocess() throws ObjectValidationException{
+ super.postprocess();
+ if(_context==null)
+ throw new ObjectValidationException();
+ for(FilterContext c:_context){
+ if(c!=null)
+ context.add(c);
+ }
+ }
+
+ public boolean matches(CharSequence text){
+ if(TextUtils.isEmpty(text))
+ return false;
+ if(pattern==null){
+ if(wholeWord)
+ pattern=Pattern.compile("\\b"+Pattern.quote(phrase)+"\\b", Pattern.CASE_INSENSITIVE);
+ else
+ pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
+ }
+ return pattern.matcher(text).find();
+ }
+
+ public boolean matches(Status status){
+ return matches(status.getContentStatus().getStrippedText());
+ }
+
+ @Override
+ public String toString(){
+ return "Filter{"+
+ "id='"+id+'\''+
+ ", phrase='"+phrase+'\''+
+ ", context="+context+
+ ", expiresAt="+expiresAt+
+ ", irreversible="+irreversible+
+ ", wholeWord="+wholeWord+
+ '}';
+ }
+
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java b/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java
deleted file mode 100644
index 751b2c0e..00000000
--- a/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.joinmastodon.android.model;
-
-import android.text.SpannableStringBuilder;
-
-import org.joinmastodon.android.GlobalUserPreferences;
-import org.joinmastodon.android.ui.text.HtmlParser;
-import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
-
-import java.util.Collections;
-
-import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
-import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
-import me.grishka.appkit.utils.V;
-
-public class ParsedAccount{
- public Account account;
- public CharSequence parsedName, parsedBio;
- public CustomEmojiHelper emojiHelper;
- public ImageLoaderRequest avatarRequest;
-
- public ParsedAccount(Account account, String accountID){
- this.account=account;
- parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
- parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
-
- emojiHelper=new CustomEmojiHelper();
- SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
- ssb.append(parsedBio);
- emojiHelper.setText(ssb);
-
- avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
- }
-}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java
index 4430bed6..89d9c8fe 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java
@@ -13,7 +13,7 @@ public class Poll extends BaseModel{
@RequiredField
public String id;
public Instant expiresAt;
- private boolean expired;
+ public boolean expired;
public boolean multiple;
public int votersCount;
public int votesCount;
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java
index 3472cc08..9e710ffc 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java
@@ -1,11 +1,14 @@
package org.joinmastodon.android.model.viewmodel;
import org.joinmastodon.android.GlobalUserPreferences;
+import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
+import java.util.Collections;
+
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@@ -14,14 +17,19 @@ public class AccountViewModel{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
- public final CharSequence parsedName;
+ public final CharSequence parsedName, parsedBio;
public final String verifiedLink;
- public AccountViewModel(Account account){
+ public AccountViewModel(Account account, String accountID){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
- emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
+ if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
+ parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
+ else
+ parsedName=account.displayName;
+ parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
+ emojiHelper.setText(parsedName);
String verifiedLink=null;
for(AccountField fld:account.fields){
if(fld.verifiedAt!=null){
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CheckableListItem.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CheckableListItem.java
new file mode 100644
index 00000000..cff521a9
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/CheckableListItem.java
@@ -0,0 +1,74 @@
+package org.joinmastodon.android.model.viewmodel;
+
+import org.joinmastodon.android.R;
+
+import java.util.function.Consumer;
+
+public class CheckableListItem extends ListItem{
+ public Style style;
+ public boolean checked;
+ public Consumer checkedChangeListener;
+
+ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject, boolean dividerAfter){
+ super(title, subtitle, iconRes, onClick, parentObject, 0, dividerAfter);
+ this.style=style;
+ this.checked=checked;
+ }
+
+ public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick){
+ this(title, subtitle, style, checked, 0, onClick, null, false);
+ }
+
+ public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick, T parentObject){
+ this(title, subtitle, style, checked, 0, onClick, parentObject, false);
+ }
+
+ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick){
+ this(title, subtitle, style, checked, iconRes, onClick, null, false);
+ }
+
+ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject){
+ this(title, subtitle, style, checked, iconRes, onClick, parentObject, false);
+ }
+
+ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick){
+ this(titleRes, subtitleRes, style, checked, 0, onClick, false);
+ }
+
+ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick, boolean dividerAfter){
+ this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter);
+ }
+
+ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick){
+ this(titleRes, subtitleRes, style, checked, iconRes, onClick, false);
+ }
+
+ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick, boolean dividerAfter){
+ super(titleRes, subtitleRes, iconRes, onClick, 0, dividerAfter);
+ this.style=style;
+ this.checked=checked;
+ }
+
+ @Override
+ public int getItemViewType(){
+ return switch(style){
+ case CHECKBOX -> R.id.list_item_checkbox;
+ case RADIO -> R.id.list_item_radio;
+ case SWITCH -> R.id.list_item_switch;
+ };
+ }
+
+ public void setChecked(boolean checked){
+ this.checked=checked;
+ }
+
+ public void toggle(){
+ checked=!checked;
+ }
+
+ public enum Style{
+ CHECKBOX,
+ RADIO,
+ SWITCH
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/ListItem.java b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/ListItem.java
new file mode 100644
index 00000000..8f4d9c73
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/ListItem.java
@@ -0,0 +1,78 @@
+package org.joinmastodon.android.model.viewmodel;
+
+import org.joinmastodon.android.R;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.StringRes;
+
+public class ListItem{
+ public String title;
+ public String subtitle;
+ @StringRes
+ public int titleRes;
+ @StringRes
+ public int subtitleRes;
+ @DrawableRes
+ public int iconRes;
+ public int colorOverrideAttr;
+ public boolean dividerAfter;
+ public Runnable onClick;
+ public boolean isEnabled=true;
+ public T parentObject;
+
+ public ListItem(String title, String subtitle, int iconRes, Runnable onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
+ this.title=title;
+ this.subtitle=subtitle;
+ this.iconRes=iconRes;
+ this.colorOverrideAttr=colorOverrideAttr;
+ this.dividerAfter=dividerAfter;
+ this.onClick=onClick;
+ this.parentObject=parentObject;
+ if(onClick==null)
+ isEnabled=false;
+ }
+
+ public ListItem(String title, String subtitle, Runnable onClick){
+ this(title, subtitle, 0, onClick, null, 0, false);
+ }
+
+ public ListItem(String title, String subtitle, Runnable onClick, T parentObject){
+ this(title, subtitle, 0, onClick, parentObject, 0, false);
+ }
+
+ public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick){
+ this(title, subtitle, iconRes, onClick, null, 0, false);
+ }
+
+ public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, T parentObject){
+ this(title, subtitle, iconRes, onClick, parentObject, 0, false);
+ }
+
+ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick){
+ this(null, null, 0, onClick, null, 0, false);
+ this.titleRes=titleRes;
+ this.subtitleRes=subtitleRes;
+ }
+
+ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
+ this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter);
+ this.titleRes=titleRes;
+ this.subtitleRes=subtitleRes;
+ }
+
+ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick){
+ this(null, null, iconRes, onClick, null, 0, false);
+ this.titleRes=titleRes;
+ this.subtitleRes=subtitleRes;
+ }
+
+ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
+ this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
+ this.titleRes=titleRes;
+ this.subtitleRes=subtitleRes;
+ }
+
+ public int getItemViewType(){
+ return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java
index f9b77101..3d3bc147 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java
@@ -111,21 +111,12 @@ public class AccountSwitcherSheet extends BottomSheet{
}
private void logOut(String accountID){
- AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
- new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
- .setCallback(new Callback<>(){
- @Override
- public void onSuccess(Object result){
- onLoggedOut(accountID);
- }
-
- @Override
- public void onError(ErrorResponse error){
- onLoggedOut(accountID);
- }
- })
- .wrapProgress(activity, R.string.loading, false)
- .exec(accountID);
+ AccountSessionManager.get(accountID).logOut(activity, ()->{
+ dismiss();
+ activity.finish();
+ Intent intent=new Intent(activity, MainActivity.class);
+ activity.startActivity(intent);
+ });
}
private void logOutAll(){
@@ -163,11 +154,6 @@ public class AccountSwitcherSheet extends BottomSheet{
}
}
- private void onLoggedOut(String accountID){
- AccountSessionManager.getInstance().removeAccount(accountID);
- dismiss();
- }
-
@Override
protected void onWindowInsetsUpdated(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java
index e140d58e..0f386d50 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/M3AlertDialogBuilder.java
@@ -2,12 +2,21 @@ package org.joinmastodon.android.ui;
import android.app.AlertDialog;
import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
+import android.widget.TextView;
+import org.joinmastodon.android.R;
+
+import androidx.annotation.StringRes;
import me.grishka.appkit.utils.V;
public class M3AlertDialogBuilder extends AlertDialog.Builder{
+ private CharSequence supportingText, title, helpText;
+ private AlertDialog alert;
+
public M3AlertDialogBuilder(Context context){
super(context);
}
@@ -18,12 +27,36 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
@Override
public AlertDialog create(){
- AlertDialog alert=super.create();
+ if(!TextUtils.isEmpty(helpText) && !TextUtils.isEmpty(supportingText))
+ throw new IllegalStateException("You can't have both help text and supporting text in the same alert");
+
+ if(!TextUtils.isEmpty(supportingText)){
+ View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_supporting_text, null);
+ TextView title=titleLayout.findViewById(R.id.title);
+ TextView subtitle=titleLayout.findViewById(R.id.subtitle);
+ title.setText(this.title);
+ subtitle.setText(supportingText);
+ setCustomTitle(titleLayout);
+ }else if(!TextUtils.isEmpty(helpText)){
+ View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_help, null);
+ TextView title=titleLayout.findViewById(R.id.title);
+ TextView helpText=titleLayout.findViewById(R.id.help_text);
+ View helpButton=titleLayout.findViewById(R.id.help);
+ title.setText(this.title);
+ helpText.setText(this.helpText);
+ helpButton.setOnClickListener(v->{
+ helpText.setVisibility(helpText.getVisibility()==View.VISIBLE ? View.GONE : View.VISIBLE);
+ helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
+ });
+ setCustomTitle(titleLayout);
+ }
+
+ alert=super.create();
alert.create();
Button btn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
if(btn!=null){
View buttonBar=(View) btn.getParent();
- buttonBar.setPadding(V.dp(16), 0, V.dp(16), V.dp(24));
+ buttonBar.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
((View)buttonBar.getParent()).setPadding(0, 0, 0, 0);
}
// hacc
@@ -49,13 +82,40 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
scrollView.setPadding(0, 0, 0, 0);
}
}
- int messageID=getContext().getResources().getIdentifier("message", "id", "android");
- if(messageID!=0){
- View message=alert.findViewById(messageID);
- if(message!=null){
- message.setPadding(message.getPaddingLeft(), message.getPaddingTop(), message.getPaddingRight(), V.dp(24));
- }
- }
return alert;
}
+
+ public M3AlertDialogBuilder setSupportingText(CharSequence text){
+ supportingText=text;
+ return this;
+ }
+
+ public M3AlertDialogBuilder setSupportingText(@StringRes int text){
+ supportingText=getContext().getString(text);
+ return this;
+ }
+
+ @Override
+ public M3AlertDialogBuilder setTitle(CharSequence title){
+ super.setTitle(title);
+ this.title=title;
+ return this;
+ }
+
+ @Override
+ public M3AlertDialogBuilder setTitle(@StringRes int title){
+ super.setTitle(title);
+ this.title=getContext().getString(title);
+ return this;
+ }
+
+ public M3AlertDialogBuilder setHelpText(CharSequence text){
+ helpText=text;
+ return this;
+ }
+
+ public M3AlertDialogBuilder setHelpText(@StringRes int text){
+ helpText=getContext().getString(text);
+ return this;
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java b/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java
index 81f05df3..afe2ab91 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java
@@ -10,6 +10,7 @@ import me.grishka.appkit.utils.V;
public class OutlineProviders{
private static final SparseArray roundedRects=new SparseArray<>();
private static final SparseArray topRoundedRects=new SparseArray<>();
+ private static final SparseArray bottomRoundedRects=new SparseArray<>();
private static final SparseArray endRoundedRects=new SparseArray<>();
public static final int RADIUS_XSMALL=4;
@@ -54,6 +55,15 @@ public class OutlineProviders{
return provider;
}
+ public static ViewOutlineProvider bottomRoundedRect(int dp){
+ ViewOutlineProvider provider=bottomRoundedRects.get(dp);
+ if(provider!=null)
+ return provider;
+ provider=new BottomRoundRectOutlineProvider(V.dp(dp));
+ bottomRoundedRects.put(dp, provider);
+ return provider;
+ }
+
public static ViewOutlineProvider endRoundedRect(int dp){
ViewOutlineProvider provider=endRoundedRects.get(dp);
if(provider!=null)
@@ -89,6 +99,19 @@ public class OutlineProviders{
}
}
+ private static class BottomRoundRectOutlineProvider extends ViewOutlineProvider{
+ private final int radius;
+
+ private BottomRoundRectOutlineProvider(int radius){
+ this.radius=radius;
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline){
+ outline.setRoundRect(0, -radius, view.getWidth(), view.getHeight(), radius);
+ }
+ }
+
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/GenericListItemsAdapter.java b/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/GenericListItemsAdapter.java
new file mode 100644
index 00000000..d5b5655b
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/GenericListItemsAdapter.java
@@ -0,0 +1,54 @@
+package org.joinmastodon.android.ui.adapters;
+
+import android.view.ViewGroup;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
+import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
+import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
+import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class GenericListItemsAdapter extends RecyclerView.Adapter>{
+ private List> items;
+
+ public GenericListItemsAdapter(List> items){
+ this.items=items;
+ }
+
+ @NonNull
+ @Override
+ public ListItemViewHolder> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
+ if(viewType==R.id.list_item_simple || viewType==R.id.list_item_simple_tinted)
+ return new SimpleListItemViewHolder(parent.getContext(), parent);
+ if(viewType==R.id.list_item_switch)
+ return new SwitchListItemViewHolder(parent.getContext(), parent);
+ if(viewType==R.id.list_item_checkbox)
+ return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
+ if(viewType==R.id.list_item_radio)
+ return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
+
+ throw new IllegalArgumentException("Unexpected view type "+viewType);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onBindViewHolder(@NonNull ListItemViewHolder> holder, int position){
+ ((ListItemViewHolder>)holder).bind(items.get(position));
+ }
+
+ @Override
+ public int getItemCount(){
+ return items.size();
+ }
+
+ @Override
+ public int getItemViewType(int position){
+ return items.get(position).getItemViewType();
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/InstanceRulesAdapter.java b/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/InstanceRulesAdapter.java
new file mode 100644
index 00000000..3a3c507e
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/adapters/InstanceRulesAdapter.java
@@ -0,0 +1,36 @@
+package org.joinmastodon.android.ui.adapters;
+
+import android.view.ViewGroup;
+
+import org.joinmastodon.android.model.Instance;
+import org.joinmastodon.android.ui.viewholders.InstanceRuleViewHolder;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class InstanceRulesAdapter extends RecyclerView.Adapter{
+ private final List rules;
+
+ public InstanceRulesAdapter(List rules){
+ this.rules=rules;
+ }
+
+ @NonNull
+ @Override
+ public InstanceRuleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
+ return new InstanceRuleViewHolder(parent);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull InstanceRuleViewHolder holder, int position){
+ holder.setPosition(position);
+ holder.bind(rules.get(position));
+ }
+
+ @Override
+ public int getItemCount(){
+ return rules.size();
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java
index 39c46498..b4666100 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountStatusDisplayItem.java
@@ -9,6 +9,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.OutlineProviders;
@@ -29,7 +30,10 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
super(parentID, parentFragment);
this.account=account;
- parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
+ if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
+ parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
+ else
+ parsedName=account.displayName;
emojiHelper.setText(parsedName);
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java
index 83d1360e..45439304 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AudioStatusDisplayItem.java
@@ -103,7 +103,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(AudioStatusDisplayItem item){
int seconds=(int)item.attachment.getDuration();
- String duration=UiUtils.formatDuration(seconds);
+ String duration=UiUtils.formatMediaDuration(seconds);
AudioPlayerService service=AudioPlayerService.getInstance();
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
forwardBtn.setVisibility(View.VISIBLE);
@@ -168,7 +168,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
setPlayButtonPlaying(false, true);
forwardBtn.setVisibility(View.INVISIBLE);
rewindBtn.setVisibility(View.INVISIBLE);
- time.setText(UiUtils.formatDuration((int)item.attachment.getDuration()));
+ time.setText(UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
}
}
@@ -187,7 +187,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
int posSeconds=(int)pos;
if(posSeconds!=lastPosSeconds){
lastPosSeconds=posSeconds;
- time.setText(UiUtils.formatDuration(posSeconds)+"/"+UiUtils.formatDuration((int)item.attachment.getDuration()));
+ time.setText(UiUtils.formatMediaDuration(posSeconds)+"/"+UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
}
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
index 6c567917..ed4b4359 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
@@ -68,7 +68,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
reblogs.setText(itemView.getResources().getQuantityString(R.plurals.x_reblogs, (int)item.status.reblogsCount, item.status.reblogsCount));
if(s.editedAt!=null){
editHistory.setVisibility(View.VISIBLE);
- editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)));
+ editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false)));
}else{
editHistory.setVisibility(View.GONE);
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
index 38c3403a..bffd9f51 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
@@ -6,13 +6,16 @@ import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.ImageView;
+import android.widget.PopupMenu;
import android.widget.TextView;
+import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
@@ -133,6 +136,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onBoostClick(View v){
+ if(GlobalUserPreferences.confirmBoost){
+ PopupMenu menu=new PopupMenu(itemView.getContext(), boost);
+ menu.getMenu().add(R.string.button_reblog);
+ menu.setOnMenuItemClickListener(item->{
+ doBoost();
+ return true;
+ });
+ menu.show();
+ }else{
+ doBoost();
+ }
+ }
+
+ private void doBoost(){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged);
boost.setSelected(item.status.reblogged);
bindButton(boost, item.status.reblogsCount);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java
index c81b38ca..0565bec3 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java
@@ -71,7 +71,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
this.status=status;
- HtmlParser.parseCustomEmoji(parsedName, user.emojis);
+ if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
+ HtmlParser.parseCustomEmoji(parsedName, user.emojis);
emojiHelper.setText(parsedName);
if(status!=null){
hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
index 782acc76..6572fe89 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java
@@ -6,7 +6,9 @@ import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
+import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
@@ -90,6 +92,7 @@ public abstract class StatusDisplayItem{
ArrayList items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
HeaderStatusDisplayItem header=null;
+ boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
if((flags & FLAG_NO_HEADER)==0){
if(status.reblog!=null){
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
@@ -104,7 +107,7 @@ public abstract class StatusDisplayItem{
}
ArrayList contentItems;
- if(!TextUtils.isEmpty(statusForContent.spoilerText)){
+ if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, statusForContent);
items.add(spoilerItem);
contentItems=spoilerItem.contentItems;
@@ -126,6 +129,8 @@ public abstract class StatusDisplayItem{
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
+ else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
+ mediaGrid.sensitiveRevealed=true;
contentItems.add(mediaGrid);
}
for(Attachment att:statusForContent.mediaAttachments){
@@ -140,7 +145,9 @@ public abstract class StatusDisplayItem{
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
}
if((flags & FLAG_NO_FOOTER)==0){
- items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
+ FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
+ footer.hideCounts=hideCounts;
+ items.add(footer);
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java
index a2a0a1da..5b7dfb1b 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java
@@ -60,7 +60,7 @@ public class MediaAttachmentViewController{
altButton.setVisibility(TextUtils.isEmpty(attachment.description) ? View.GONE : View.VISIBLE);
}
if(type==MediaGridStatusDisplayItem.GridItemType.VIDEO){
- duration.setText(UiUtils.formatDuration((int)attachment.getDuration()));
+ duration.setText(UiUtils.formatMediaDuration((int)attachment.getDuration()));
}
didClear=false;
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
index a8ed5d22..3a9577fd 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
@@ -71,6 +71,7 @@ import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Method;
import java.time.Instant;
+import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@@ -99,6 +100,7 @@ import okhttp3.MediaType;
public class UiUtils{
private static Handler mainHandler=new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM");
+ private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
private UiUtils(){}
@@ -144,21 +146,52 @@ public class UiUtils{
}
}
- public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
+ public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant, boolean relativeHours){
long t=instant.toEpochMilli();
- long now=System.currentTimeMillis();
- long diff=now-t;
- if(diff<1000L){
+ long diff=System.currentTimeMillis()-t;
+ if(diff<1000L && diff>-1000L){
return context.getString(R.string.time_just_now);
- }else if(diff<60_000L){
- int secs=(int)(diff/1000L);
- return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
- }else if(diff<3600_000L){
- int mins=(int)(diff/60_000L);
- return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
+ }else if(diff>0){
+ if(diff<60_000L){
+ int secs=(int)(diff/1000L);
+ return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
+ }else if(diff<3600_000L){
+ int mins=(int)(diff/60_000L);
+ return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
+ }else if(relativeHours && diff<24*3600_000L){
+ int hours=(int)(diff/3600_000L);
+ return context.getResources().getQuantityString(R.plurals.x_hours_ago, hours, hours);
+ }
}else{
- return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
+ if(diff>-60_000L){
+ int secs=-(int)(diff/1000L);
+ return context.getResources().getQuantityString(R.plurals.in_x_seconds, secs, secs);
+ }else if(diff>-3600_000L){
+ int mins=-(int)(diff/60_000L);
+ return context.getResources().getQuantityString(R.plurals.in_x_minutes, mins, mins);
+ }else if(relativeHours && diff>-24*3600_000L){
+ int hours=-(int)(diff/3600_000L);
+ return context.getResources().getQuantityString(R.plurals.in_x_hours, hours, hours);
+ }
}
+ ZonedDateTime dt=instant.atZone(ZoneId.systemDefault());
+ ZonedDateTime now=ZonedDateTime.now();
+ String formattedTime=TIME_FORMATTER.format(dt);
+ String formattedDate;
+ LocalDate today=now.toLocalDate();
+ LocalDate date=dt.toLocalDate();
+ if(date.equals(today)){
+ formattedDate=context.getString(R.string.today);
+ }else if(date.equals(today.minusDays(1))){
+ formattedDate=context.getString(R.string.yesterday);
+ }else if(date.equals(today.plusDays(1))){
+ formattedDate=context.getString(R.string.tomorrow);
+ }else if(date.getYear()==today.getYear()){
+ formattedDate=DATE_FORMATTER_SHORT.format(dt);
+ }else{
+ formattedDate=DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
+ }
+ return context.getString(R.string.date_at_time, formattedDate, formattedTime);
}
public static String formatTimeLeft(Context context, Instant instant){
@@ -317,7 +350,7 @@ public class UiUtils{
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
- showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
+ showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
@@ -399,24 +432,26 @@ public class UiUtils{
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer resultCallback){
- showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, ()->{
- new DeleteStatus(status.id)
- .setCallback(new Callback<>(){
- @Override
- public void onSuccess(Status result){
- resultCallback.accept(result);
- AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
- E.post(new StatusDeletedEvent(status.id, accountID));
- }
+ Runnable delete=()->new DeleteStatus(status.id)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Status result){
+ resultCallback.accept(result);
+ AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
+ E.post(new StatusDeletedEvent(status.id, accountID));
+ }
- @Override
- public void onError(ErrorResponse error){
- error.showToast(activity);
- }
- })
- .wrapProgress(activity, R.string.deleting, false)
- .exec(accountID);
- });
+ @Override
+ public void onError(ErrorResponse error){
+ error.showToast(activity);
+ }
+ })
+ .wrapProgress(activity, R.string.deleting, false)
+ .exec(accountID);
+ if(GlobalUserPreferences.confirmDeletePost)
+ showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, delete);
+ else
+ delete.run();
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
@@ -488,25 +523,32 @@ public class UiUtils{
}else if(relationship.muting){
confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
}else{
- progressCallback.accept(true);
- new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
- .setCallback(new Callback<>(){
- @Override
- public void onSuccess(Relationship result){
- resultCallback.accept(result);
- progressCallback.accept(false);
- if(!result.following && !result.requested){
- E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
+ Runnable action=()->{
+ progressCallback.accept(true);
+ new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
+ .setCallback(new Callback<>(){
+ @Override
+ public void onSuccess(Relationship result){
+ resultCallback.accept(result);
+ progressCallback.accept(false);
+ if(!result.following && !result.requested){
+ E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
+ }
}
- }
- @Override
- public void onError(ErrorResponse error){
- error.showToast(activity);
- progressCallback.accept(false);
- }
- })
- .exec(accountID);
+ @Override
+ public void onError(ErrorResponse error){
+ error.showToast(activity);
+ progressCallback.accept(false);
+ }
+ })
+ .exec(accountID);
+ };
+ if(relationship.following && GlobalUserPreferences.confirmUnfollow){
+ showConfirmationAlert(activity, null, activity.getString(R.string.unfollow_confirmation, account.getDisplayUsername()), activity.getString(R.string.unfollow), action);
+ }else{
+ action.run();
+ }
}
}
@@ -586,9 +628,9 @@ public class UiUtils{
public static void setUserPreferredTheme(Context context){
context.setTheme(switch(GlobalUserPreferences.theme){
- case AUTO -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
+ case AUTO -> R.style.Theme_Mastodon_AutoLightDark;
case LIGHT -> R.style.Theme_Mastodon_Light;
- case DARK -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
+ case DARK -> R.style.Theme_Mastodon_Dark;
});
}
@@ -718,7 +760,7 @@ public class UiUtils{
}
@SuppressLint("DefaultLocale")
- public static String formatDuration(int seconds){
+ public static String formatMediaDuration(int seconds){
if(seconds>=3600)
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
else
@@ -750,4 +792,20 @@ public class UiUtils{
}
return insets;
}
+
+ public static String formatDuration(Context context, int seconds){
+ if(seconds<3600){
+ int minutes=seconds/60;
+ return context.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
+ }else if(seconds<24*3600){
+ int hours=seconds/3600;
+ return context.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
+ }else if(seconds>=7*24*3600 && seconds%(7*24*3600)<24*3600){
+ int weeks=seconds/(7*24*3600);
+ return context.getResources().getQuantityString(R.plurals.x_weeks, weeks, weeks);
+ }else{
+ int days=seconds/(24*3600);
+ return context.getResources().getQuantityString(R.plurals.x_days, days, days);
+ }
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java
index 4d89e0e0..5ccf39c6 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeAutocompleteViewController.java
@@ -19,6 +19,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResults;
+import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -58,7 +59,7 @@ public class ComposeAutocompleteViewController{
private FrameLayout contentView;
private UsableRecyclerView list;
private ListImageLoaderWrapper imgLoader;
- private List users=Collections.emptyList();
+ private List users=Collections.emptyList();
private List hashtags=Collections.emptyList();
private List emojis=Collections.emptyList();
private Mode mode;
@@ -226,8 +227,8 @@ public class ComposeAutocompleteViewController{
@Override
public void onSuccess(SearchResults result){
currentRequest=null;
- List oldList=users;
- users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
+ List oldList=users;
+ users=result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList());
if(isLoading){
isLoading=false;
if(users.size()>=LOADING_FAKE_USER_COUNT){
@@ -313,7 +314,7 @@ public class ComposeAutocompleteViewController{
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
- WrappedAccount a=users.get(position);
+ AccountViewModel a=users.get(position);
if(image==0)
return a.avaRequest;
return a.emojiHelper.getImageRequest(image-1);
@@ -325,7 +326,7 @@ public class ComposeAutocompleteViewController{
}
}
- private class UserViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
+ private class UserViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
protected final ImageView ava;
protected final TextView username;
@@ -338,7 +339,7 @@ public class ComposeAutocompleteViewController{
}
@Override
- public void onBind(WrappedAccount item){
+ public void onBind(AccountViewModel item){
username.setText("@"+item.account.acct);
}
@@ -483,21 +484,6 @@ public class ComposeAutocompleteViewController{
}
}
- private static class WrappedAccount{
- private Account account;
- private CharSequence parsedName;
- private CustomEmojiHelper emojiHelper;
- private ImageLoaderRequest avaRequest;
-
- public WrappedAccount(Account account){
- this.account=account;
- parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
- emojiHelper=new CustomEmojiHelper();
- emojiHelper.setText(parsedName);
- avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
- }
- }
-
private static class WrappedEmoji{
private Emoji emoji;
private ImageLoaderRequest request;
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java
index badcdd50..ff3239ce 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java
@@ -89,10 +89,32 @@ public class ComposeLanguageAlertViewController{
}
if(previouslySelected!=null){
- if((previouslySelected.index{
pollDuration=POLL_LENGTH_OPTIONS[chosenOption[0]];
- pollDurationValue.setText(formatPollDuration(pollDuration));
+ pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
})
.setNegativeButton(R.string.cancel, null)
.show();
}
- private String formatPollDuration(int seconds){
- if(seconds<3600){
- int minutes=seconds/60;
- return fragment.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
- }else if(seconds<24*3600){
- int hours=seconds/3600;
- return fragment.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
- }else{
- int days=seconds/(24*3600);
- return fragment.getResources().getQuantityString(R.plurals.x_days, days, days);
- }
- }
-
private void showPollStyleAlert(){
final int[] option={pollIsMultipleChoice ? R.id.multiple_choice : R.id.single_choice};
AlertDialog alert=new M3AlertDialogBuilder(fragment.getActivity())
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java
index 5cec3b0f..6dca58ce 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java
@@ -110,6 +110,8 @@ public class AccountViewHolder extends BindableViewHolder impl
}
public void bindRelationship(){
+ if(relationships==null)
+ return;
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
@@ -193,6 +195,8 @@ public class AccountViewHolder extends BindableViewHolder impl
}
private void onButtonClick(View v){
+ if(relationships==null)
+ return;
ProgressDialog progress=new ProgressDialog(fragment.getActivity());
progress.setMessage(fragment.getString(R.string.loading));
progress.setCancelable(false);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckableListItemViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckableListItemViewHolder.java
new file mode 100644
index 00000000..d649be96
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckableListItemViewHolder.java
@@ -0,0 +1,23 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.ui.views.CheckableLinearLayout;
+
+public abstract class CheckableListItemViewHolder extends ListItemViewHolder>{
+ protected final CheckableLinearLayout checkableLayout;
+
+ public CheckableListItemViewHolder(Context context, ViewGroup parent){
+ super(context, R.layout.item_generic_list_checkable, parent);
+ checkableLayout=(CheckableLinearLayout) itemView;
+ }
+
+ @Override
+ public void onBind(CheckableListItem> item){
+ super.onBind(item);
+ checkableLayout.setChecked(item.checked);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckboxOrRadioListItemViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckboxOrRadioListItemViewHolder.java
new file mode 100644
index 00000000..a91e0c1e
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/CheckboxOrRadioListItemViewHolder.java
@@ -0,0 +1,25 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+
+import me.grishka.appkit.utils.V;
+
+public class CheckboxOrRadioListItemViewHolder extends CheckableListItemViewHolder{
+ public CheckboxOrRadioListItemViewHolder(Context context, ViewGroup parent, boolean radio){
+ super(context, parent);
+ View iconView=new View(context);
+ iconView.setDuplicateParentStateEnabled(true);
+ CompoundButton terribleHack=radio ? new RadioButton(context) : new CheckBox(context);
+ iconView.setBackground(terribleHack.getButtonDrawable());
+ LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(32), V.dp(32));
+ lp.setMarginStart(V.dp(12));
+ lp.setMarginEnd(V.dp(4));
+ checkableLayout.addView(iconView, lp);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/InstanceRuleViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/InstanceRuleViewHolder.java
new file mode 100644
index 00000000..31e8595b
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/InstanceRuleViewHolder.java
@@ -0,0 +1,36 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.annotation.SuppressLint;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.Instance;
+import org.joinmastodon.android.ui.text.HtmlParser;
+
+import me.grishka.appkit.utils.BindableViewHolder;
+
+public class InstanceRuleViewHolder extends BindableViewHolder{
+ private final TextView text, number;
+ private int position;
+
+ public InstanceRuleViewHolder(ViewGroup parent){
+ super(parent.getContext(), R.layout.item_server_rule, parent);
+ text=findViewById(R.id.text);
+ number=findViewById(R.id.number);
+ }
+
+ public void setPosition(int position){
+ this.position=position;
+ }
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public void onBind(Instance.Rule item){
+ if(item.parsedText==null){
+ item.parsedText=HtmlParser.parseLinks(item.text);
+ }
+ text.setText(item.parsedText);
+ number.setText(String.format("%d", position+1));
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/ListItemViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/ListItemViewHolder.java
new file mode 100644
index 00000000..7b138c48
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/ListItemViewHolder.java
@@ -0,0 +1,80 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+import org.joinmastodon.android.ui.utils.UiUtils;
+
+import me.grishka.appkit.utils.BindableViewHolder;
+import me.grishka.appkit.utils.V;
+import me.grishka.appkit.views.UsableRecyclerView;
+
+public abstract class ListItemViewHolder> extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{
+ protected final TextView title;
+ protected final TextView subtitle;
+ protected final ImageView icon;
+ protected final LinearLayout view;
+
+ public ListItemViewHolder(Context context, int layout, ViewGroup parent){
+ super(context, layout, parent);
+ title=findViewById(R.id.title);
+ subtitle=findViewById(R.id.subtitle);
+ icon=findViewById(R.id.icon);
+ view=(LinearLayout) itemView;
+ }
+
+ @Override
+ public void onBind(T item){
+ if(TextUtils.isEmpty(item.title))
+ title.setText(item.titleRes);
+ else
+ title.setText(item.title);
+
+ if(TextUtils.isEmpty(item.subtitle) && item.subtitleRes==0){
+ subtitle.setVisibility(View.GONE);
+ title.setMaxLines(2);
+ view.setMinimumHeight(V.dp(56));
+ }else{
+ subtitle.setVisibility(View.VISIBLE);
+ title.setMaxLines(1);
+ view.setMinimumHeight(V.dp(72));
+ if(TextUtils.isEmpty(item.subtitle))
+ subtitle.setText(item.subtitleRes);
+ else
+ subtitle.setText(item.subtitle);
+ }
+
+ if(item.iconRes!=0){
+ icon.setVisibility(View.VISIBLE);
+ icon.setImageResource(item.iconRes);
+ }else{
+ icon.setVisibility(View.GONE);
+ }
+
+ if(item.colorOverrideAttr!=0){
+ int color=UiUtils.getThemeColor(view.getContext(), item.colorOverrideAttr);
+ title.setTextColor(color);
+ icon.setImageTintList(ColorStateList.valueOf(color));
+ }
+
+ view.setAlpha(item.isEnabled ? 1 : .4f);
+ }
+
+ @Override
+ public boolean isEnabled(){
+ return item.isEnabled;
+ }
+
+ @Override
+ public void onClick(){
+ item.onClick.run();
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SimpleListItemViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SimpleListItemViewHolder.java
new file mode 100644
index 00000000..901e87c2
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SimpleListItemViewHolder.java
@@ -0,0 +1,13 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.model.viewmodel.ListItem;
+
+public class SimpleListItemViewHolder extends ListItemViewHolder>{
+ public SimpleListItemViewHolder(Context context, ViewGroup parent){
+ super(context, R.layout.item_generic_list, parent);
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SwitchListItemViewHolder.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SwitchListItemViewHolder.java
new file mode 100644
index 00000000..85cefd00
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/SwitchListItemViewHolder.java
@@ -0,0 +1,43 @@
+package org.joinmastodon.android.ui.viewholders;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.joinmastodon.android.model.viewmodel.CheckableListItem;
+import org.joinmastodon.android.ui.views.M3Switch;
+
+import me.grishka.appkit.utils.V;
+
+public class SwitchListItemViewHolder extends CheckableListItemViewHolder{
+ private final M3Switch sw;
+ private boolean ignoreListener;
+
+ public SwitchListItemViewHolder(Context context, ViewGroup parent){
+ super(context, parent);
+ sw=new M3Switch(context);
+ LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(52), V.dp(32));
+ lp.gravity=Gravity.TOP;
+ lp.setMarginStart(V.dp(16));
+ checkableLayout.addView(sw, lp);
+ sw.setOnCheckedChangeListener((buttonView, isChecked)->{
+ if(ignoreListener)
+ return;
+ if(item.checkedChangeListener!=null)
+ item.checkedChangeListener.accept(isChecked);
+ else
+ item.checked=isChecked;
+ });
+ sw.setClickable(true);
+ }
+
+ @Override
+ public void onBind(CheckableListItem> item){
+ super.onBind(item);
+ ignoreListener=true;
+ sw.setChecked(item.checked);
+ sw.setEnabled(item.isEnabled);
+ ignoreListener=false;
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java
index b02f6865..54746df1 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java
@@ -98,7 +98,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
errorView=new LinkedTextView(getContext());
errorView.setTextAppearance(R.style.m3_body_small);
- errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
+ errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
errorView.setPadding(dp(16), dp(4), dp(16), 0);
@@ -106,6 +106,10 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
addView(errorView);
}
+ public void updateHint(){
+ label.setText(edit.getHint());
+ }
+
private void onTextChanged(Editable text){
if(errorState){
errorView.setVisibility(View.GONE);
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java
index 10c8e19b..bdcd3182 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java
@@ -1,8 +1,13 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
+import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
+import android.webkit.WebView;
+import android.widget.ScrollView;
+
+import org.joinmastodon.android.R;
import java.util.function.Supplier;
@@ -10,19 +15,22 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class NestedRecyclerScrollView extends CustomScrollView{
- private Supplier scrollableChildSupplier;
+ private Supplier scrollableChildSupplier;
private boolean takePriorityOverChildViews;
public NestedRecyclerScrollView(Context context){
- super(context);
+ this(context, null);
}
public NestedRecyclerScrollView(Context context, AttributeSet attrs){
- super(context, attrs);
+ this(context, attrs, 0);
}
- public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){
- super(context, attrs, defStyleAttr);
+ public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyle){
+ super(context, attrs, defStyle);
+ TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.NestedRecyclerScrollView);
+ takePriorityOverChildViews=ta.getBoolean(R.styleable.NestedRecyclerScrollView_takePriorityOverChildViews, false);
+ ta.recycle();
}
@Override
@@ -33,7 +41,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
consumed[1]=dy;
return;
}
- }else if((dy<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (dy>0 && !isScrolledToBottom())){
+ }else if((dy<0 && isScrolledToTop(target)) || (dy>0 && !isScrolledToBottom())){
scrollBy(0, dy);
consumed[1]=dy;
return;
@@ -48,7 +56,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
fling((int)velY);
return true;
}
- }else if((velY<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (velY>0 && !isScrolledToBottom())){
+ }else if((velY<0 && isScrolledToTop(target)) || (velY>0 && !isScrolledToBottom())){
fling((int) velY);
return true;
}
@@ -59,22 +67,40 @@ public class NestedRecyclerScrollView extends CustomScrollView{
return !canScrollVertically(1);
}
- private boolean isScrolledToTop(RecyclerView rv){
- final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
- return lm.findFirstVisibleItemPosition()==0
- && lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
+ private boolean isScrolledToTop(View view){
+ if(view instanceof RecyclerView rv){
+ final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
+ return lm.findFirstVisibleItemPosition()==0
+ && lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
+ }
+ return !view.canScrollVertically(-1);
}
- public void setScrollableChildSupplier(Supplier scrollableChildSupplier){
+ public void setScrollableChildSupplier(Supplier scrollableChildSupplier){
this.scrollableChildSupplier=scrollableChildSupplier;
}
@Override
protected boolean onScrollingHitEdge(float velocity){
if(velocity>0 || takePriorityOverChildViews){
- RecyclerView view=scrollableChildSupplier.get();
- if(view!=null){
- return view.fling(0, (int) velocity);
+ View view=scrollableChildSupplier==null ? null : scrollableChildSupplier.get();
+ if(view instanceof RecyclerView rv){
+ return rv.fling(0, (int) velocity);
+ }else if(view instanceof ScrollView sv){
+ if(sv.canScrollVertically((int)velocity)){
+ sv.fling((int)velocity);
+ return true;
+ }
+ }else if(view instanceof CustomScrollView sv){
+ if(sv.canScrollVertically((int)velocity)){
+ sv.fling((int)velocity);
+ return true;
+ }
+ }else if(view instanceof WebView wv){
+ if(wv.canScrollVertically((int)velocity)){
+ wv.flingScroll(0, (int)velocity);
+ return true;
+ }
}
}
return false;
diff --git a/mastodon/src/main/java/org/joinmastodon/android/updater/GithubSelfUpdater.java b/mastodon/src/main/java/org/joinmastodon/android/updater/GithubSelfUpdater.java
index 810c516d..7e26508a 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/updater/GithubSelfUpdater.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/updater/GithubSelfUpdater.java
@@ -7,6 +7,7 @@ import org.joinmastodon.android.BuildConfig;
public abstract class GithubSelfUpdater{
private static GithubSelfUpdater instance;
+ public static boolean forceUpdate;
public static GithubSelfUpdater getInstance(){
if(instance==null){
@@ -20,7 +21,7 @@ public abstract class GithubSelfUpdater{
}
public static boolean needSelfUpdating(){
- return BuildConfig.BUILD_TYPE.equals("githubRelease");
+ return BuildConfig.BUILD_TYPE.equals("githubRelease") || BuildConfig.BUILD_TYPE.equals("githubDebug");
}
public abstract void maybeCheckForUpdates();
@@ -39,6 +40,8 @@ public abstract class GithubSelfUpdater{
public abstract void handleIntentFromInstaller(Intent intent, Activity activity);
+ public abstract void reset();
+
public enum UpdateState{
NO_UPDATE,
CHECKING,
diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java
index 62f49ff5..160097cb 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java
@@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.view.View;
@@ -27,6 +28,7 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
private Animator currentPanelsAnim;
private List views;
private FragmentRootLinearLayout fragmentRootLayout;
+ private Rect tmpRect=new Rect();
public ElevationOnScrollListener(FragmentRootLinearLayout fragmentRootLayout, View... views){
this(fragmentRootLayout, Arrays.asList(views));
@@ -70,9 +72,14 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
}
}
+ private int getRecyclerChildDecoratedTop(RecyclerView rv, View child){
+ rv.getDecoratedBoundsWithMargins(child, tmpRect);
+ return tmpRect.top;
+ }
+
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
- boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
+ boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && getRecyclerChildDecoratedTop(recyclerView, recyclerView.getChildAt(0))==recyclerView.getPaddingTop());
handleScroll(recyclerView.getContext(), newAtTop);
}
@@ -120,4 +127,8 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
currentPanelsAnim=set;
}
}
+
+ public int getCurrentStatusBarColor(){
+ return fragmentRootLayout.getStatusBarColor();
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java
index cf9e0829..c9a6e9a0 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java
@@ -1,7 +1,8 @@
package org.joinmastodon.android.utils;
import org.joinmastodon.android.api.session.AccountSessionManager;
-import org.joinmastodon.android.model.Filter;
+import org.joinmastodon.android.model.FilterContext;
+import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import java.util.List;
@@ -9,19 +10,19 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
public class StatusFilterPredicate implements Predicate{
- private final List filters;
+ private final List filters;
- public StatusFilterPredicate(List filters){
+ public StatusFilterPredicate(List filters){
this.filters=filters;
}
- public StatusFilterPredicate(String accountID, Filter.FilterContext context){
+ public StatusFilterPredicate(String accountID, FilterContext context){
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
}
@Override
public boolean test(Status status){
- for(Filter filter:filters){
+ for(LegacyFilter filter:filters){
if(filter.matches(status))
return false;
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/ViewImageLoaderHolderTarget.java b/mastodon/src/main/java/org/joinmastodon/android/utils/ViewImageLoaderHolderTarget.java
new file mode 100644
index 00000000..2725df0d
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/utils/ViewImageLoaderHolderTarget.java
@@ -0,0 +1,31 @@
+package org.joinmastodon.android.utils;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
+import me.grishka.appkit.imageloader.ViewImageLoader;
+
+public class ViewImageLoaderHolderTarget implements ViewImageLoader.Target{
+ private final ImageLoaderViewHolder holder;
+ private final int imageIndex;
+
+ public ViewImageLoaderHolderTarget(ImageLoaderViewHolder holder, int imageIndex){
+ this.holder=holder;
+ this.imageIndex=imageIndex;
+ }
+
+ @Override
+ public void setImageDrawable(Drawable d){
+ if(d==null)
+ holder.clearImage(imageIndex);
+ else
+ holder.setImage(imageIndex, d);
+ }
+
+ @Override
+ public View getView(){
+ return ((RecyclerView.ViewHolder)holder).itemView;
+ }
+}
diff --git a/mastodon/src/main/res/anim/fade_out_fast.xml b/mastodon/src/main/res/anim/fade_out_fast.xml
index de985e23..3f09d564 100644
--- a/mastodon/src/main/res/anim/fade_out_fast.xml
+++ b/mastodon/src/main/res/anim/fade_out_fast.xml
@@ -1,6 +1,6 @@
\ No newline at end of file
diff --git a/mastodon/src/main/res/color/selectable_icon_tint.xml b/mastodon/src/main/res/color/selectable_icon_tint.xml
new file mode 100644
index 00000000..83c7d0a6
--- /dev/null
+++ b/mastodon/src/main/res/color/selectable_icon_tint.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/drawable/bg_settings_banner.xml b/mastodon/src/main/res/drawable/bg_settings_banner.xml
new file mode 100644
index 00000000..5e5a2f5a
--- /dev/null
+++ b/mastodon/src/main/res/drawable/bg_settings_banner.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/drawable/ic_actionmode_close.xml b/mastodon/src/main/res/drawable/ic_actionmode_close.xml
new file mode 100644
index 00000000..222363cd
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_actionmode_close.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_alt_24px.xml b/mastodon/src/main/res/drawable/ic_alt_24px.xml
new file mode 100644
index 00000000..ce9c3b96
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_alt_24px.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_animation_24px.xml b/mastodon/src/main/res/drawable/ic_animation_24px.xml
new file mode 100644
index 00000000..2ebcb150
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_animation_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_apk_install_24px.xml b/mastodon/src/main/res/drawable/ic_apk_install_24px.xml
new file mode 100644
index 00000000..19fba8f7
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_apk_install_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_app_badging_24px.xml b/mastodon/src/main/res/drawable/ic_app_badging_24px.xml
new file mode 100644
index 00000000..c626b498
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_app_badging_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_dark_mode_24px.xml b/mastodon/src/main/res/drawable/ic_dark_mode_24px.xml
new file mode 100644
index 00000000..ca5cc6a7
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_dark_mode_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_dns_24px.xml b/mastodon/src/main/res/drawable/ic_dns_24px.xml
new file mode 100644
index 00000000..dcc0d4e6
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_dns_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_emoticon_24px.xml b/mastodon/src/main/res/drawable/ic_emoticon_24px.xml
new file mode 100644
index 00000000..f492a254
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_emoticon_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_filter_alt_24px.xml b/mastodon/src/main/res/drawable/ic_filter_alt_24px.xml
new file mode 100644
index 00000000..006d21e4
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_filter_alt_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_group_24px.xml b/mastodon/src/main/res/drawable/ic_group_24px.xml
new file mode 100644
index 00000000..21147d83
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_group_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_help_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_help_fill1_24px.xml
new file mode 100644
index 00000000..6bb3099e
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_help_fill1_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_help_selectable.xml b/mastodon/src/main/res/drawable/ic_help_selectable.xml
new file mode 100644
index 00000000..a7e8ad04
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_help_selectable.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/drawable/ic_info_24px.xml b/mastodon/src/main/res/drawable/ic_info_24px.xml
new file mode 100644
index 00000000..367c171e
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_info_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_lock_24px.xml b/mastodon/src/main/res/drawable/ic_lock_24px.xml
new file mode 100644
index 00000000..62392637
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_lock_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_mail_24px.xml b/mastodon/src/main/res/drawable/ic_mail_24px.xml
new file mode 100644
index 00000000..ad21d51d
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_mail_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_no_adult_content_24px.xml b/mastodon/src/main/res/drawable/ic_no_adult_content_24px.xml
new file mode 100644
index 00000000..acc69102
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_no_adult_content_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_notifications_paused_24px.xml b/mastodon/src/main/res/drawable/ic_notifications_paused_24px.xml
new file mode 100644
index 00000000..c0c55d00
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_notifications_paused_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_open_in_browser_24px.xml b/mastodon/src/main/res/drawable/ic_open_in_browser_24px.xml
new file mode 100644
index 00000000..2e7b8d01
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_open_in_browser_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_person_remove_24px.xml b/mastodon/src/main/res/drawable/ic_person_remove_24px.xml
new file mode 100644
index 00000000..abe85646
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_person_remove_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_settings_24_badged.xml b/mastodon/src/main/res/drawable/ic_settings_24_badged.xml
index de24a3c0..9c1148ae 100644
--- a/mastodon/src/main/res/drawable/ic_settings_24_badged.xml
+++ b/mastodon/src/main/res/drawable/ic_settings_24_badged.xml
@@ -1,10 +1,10 @@
-
+
-
-
-
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/drawable/ic_settings_24px.xml b/mastodon/src/main/res/drawable/ic_settings_24px.xml
new file mode 100644
index 00000000..0559faf5
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_settings_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_social_leaderboard_24px.xml b/mastodon/src/main/res/drawable/ic_social_leaderboard_24px.xml
new file mode 100644
index 00000000..801cefa9
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_social_leaderboard_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/drawable/ic_style_24px.xml b/mastodon/src/main/res/drawable/ic_style_24px.xml
new file mode 100644
index 00000000..2e47386f
--- /dev/null
+++ b/mastodon/src/main/res/drawable/ic_style_24px.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mastodon/src/main/res/layout/alert_title_with_help.xml b/mastodon/src/main/res/layout/alert_title_with_help.xml
new file mode 100644
index 00000000..2e0eecda
--- /dev/null
+++ b/mastodon/src/main/res/layout/alert_title_with_help.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/alert_title_with_supporting_text.xml b/mastodon/src/main/res/layout/alert_title_with_supporting_text.xml
new file mode 100644
index 00000000..27bc56c6
--- /dev/null
+++ b/mastodon/src/main/res/layout/alert_title_with_supporting_text.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/floating_hint_edit_text.xml b/mastodon/src/main/res/layout/floating_hint_edit_text.xml
new file mode 100644
index 00000000..60d7df2a
--- /dev/null
+++ b/mastodon/src/main/res/layout/floating_hint_edit_text.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/mastodon/src/main/res/layout/fragment_settings_server.xml b/mastodon/src/main/res/layout/fragment_settings_server.xml
new file mode 100644
index 00000000..e5efdcab
--- /dev/null
+++ b/mastodon/src/main/res/layout/fragment_settings_server.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/item_generic_list.xml b/mastodon/src/main/res/layout/item_generic_list.xml
new file mode 100644
index 00000000..089e62a8
--- /dev/null
+++ b/mastodon/src/main/res/layout/item_generic_list.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/item_generic_list_checkable.xml b/mastodon/src/main/res/layout/item_generic_list_checkable.xml
new file mode 100644
index 00000000..d8145b2e
--- /dev/null
+++ b/mastodon/src/main/res/layout/item_generic_list_checkable.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/item_generic_list_content.xml b/mastodon/src/main/res/layout/item_generic_list_content.xml
new file mode 100644
index 00000000..b7ee8f6f
--- /dev/null
+++ b/mastodon/src/main/res/layout/item_generic_list_content.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/item_settings_banner.xml b/mastodon/src/main/res/layout/item_settings_banner.xml
new file mode 100644
index 00000000..0de195da
--- /dev/null
+++ b/mastodon/src/main/res/layout/item_settings_banner.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/menu/home.xml b/mastodon/src/main/res/menu/home.xml
index 5d42d671..ba9978f4 100644
--- a/mastodon/src/main/res/menu/home.xml
+++ b/mastodon/src/main/res/menu/home.xml
@@ -2,7 +2,7 @@
\ No newline at end of file
diff --git a/mastodon/src/main/res/menu/settings_edit_filter.xml b/mastodon/src/main/res/menu/settings_edit_filter.xml
new file mode 100644
index 00000000..97596f6d
--- /dev/null
+++ b/mastodon/src/main/res/menu/settings_edit_filter.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/menu/settings_filter_words.xml b/mastodon/src/main/res/menu/settings_filter_words.xml
new file mode 100644
index 00000000..f702e25e
--- /dev/null
+++ b/mastodon/src/main/res/menu/settings_filter_words.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/menu/settings_filter_words_action_mode.xml b/mastodon/src/main/res/menu/settings_filter_words_action_mode.xml
new file mode 100644
index 00000000..87e8ad39
--- /dev/null
+++ b/mastodon/src/main/res/menu/settings_filter_words_action_mode.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/values-night/styles.xml b/mastodon/src/main/res/values-night/styles.xml
index 2b7449eb..00190b47 100644
--- a/mastodon/src/main/res/values-night/styles.xml
+++ b/mastodon/src/main/res/values-night/styles.xml
@@ -1,5 +1,4 @@
-
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml
index d6a20167..0db7c42a 100644
--- a/mastodon/src/main/res/values/attrs.xml
+++ b/mastodon/src/main/res/values/attrs.xml
@@ -62,4 +62,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml
index 6de4b984..1000a0b5 100644
--- a/mastodon/src/main/res/values/ids.xml
+++ b/mastodon/src/main/res/values/ids.xml
@@ -16,4 +16,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml
index 6a3041ed..52985572 100644
--- a/mastodon/src/main/res/values/strings.xml
+++ b/mastodon/src/main/res/values/strings.xml
@@ -127,7 +127,7 @@
Vote
Tap to reveal
Delete
- Delete Post
+ Delete post
Are you sure you want to delete this post?
Deleting…
Audio playback
@@ -240,8 +240,8 @@
File %s is of an unsupported type
File %1$s exceeds the size limit of %2$s MB
- Visual appearance
- Automatic
+ Appearance
+ Use device appearance
Light
Dark
True black mode
@@ -503,4 +503,132 @@
You’ve already blocked this user, so there’s nothing else you need to do.\n\nThanks for helping keep Mastodon a safe place for everyone!
Blocked %s
Mark all as read
+ Display
+ Filters
+
+ Overview, rules, moderators
+
+ About %s
+ Default post language
+ Add alt text reminders
+ Ask before unfollowing someone
+ Ask before boosting
+ Ask before deleting posts
+ Pause all
+ Off
+ Anyone
+ People who follow you
+ People you follow
+ No one
+ Get notifications from
+ Mentions and replies
+ Pause all notifications
+
+ - %d week
+ - %d weeks
+
+
+ %1$s at %2$s
+ today
+ yesterday
+ tomorrow
+
+ Ends %s
+
+ Notifications will resume %s.
+ Resume now
+ Go to notification settings
+ About
+ Rules
+ Administrator
+ Message admin
+ Turn on notifications from your device settings to see updates from anywhere.
+ Even more settings
+ Show content warnings
+ Cover up media marked as sensitive
+ Post interaction counts
+ Custom emoji in display names
+
+ - in %d second
+ - in %d seconds
+
+
+ - in %d minute
+ - in %d minutes
+
+
+ - in %d hour
+ - in %d hours
+
+
+ - %d hour ago
+ - %d hours ago
+
+ Media missing alt text
+
+ - %s of your images is missing alt text. Post anyway?
+ - %s of your images are missing alt text. Post anyway?
+
+
+ - %s of your media attachments is missing alt text. Post anyway?
+ - %s of your media attachments are missing alt text. Post anyway?
+
+ One
+ Two
+ Three
+ Four
+ Post
+
+ Unfollow %s?
+ Active
+ Inactive
+ Add filter
+ Edit filter
+ Duration
+ Muted words
+ Mute from
+ Show with content warning
+ Still show posts that match this filter, but behind a content warning
+ Delete filter
+ Forever
+
+ Ends %s
+
+ - %d muted word or phrase
+ - %d muted words or phrases
+
+ %1$s and %2$s
+ %1$s, %2$s, and %3$s
+ %1$s, %2$s, and %3$d more
+ Home & lists
+ Notifications
+ Public timelines
+ Threads & replies
+ Profiles
+ Title
+ Delete filter “%s”?
+ This filter will be deleted from your account on all your devices.
+ Add muted word
+ Edit muted word
+ Add
+ Word or phrase
+ Words are case-insensitive and match full words only.\n\nIf you filter the keyword “Apple,” it will hide posts containing “apple” or “aPpLe” but not “pineapple.”
+ Delete word “%s”?
+ Select
+ Select all
+ Filter duration
+ Custom
+
+ - Delete %d word?
+ - Delete %d words?
+
+
+ - %d selected
+ - %d selected
+
+ Cannot be blank
+ Already in the list
+ App update ready
+ Version %s
+ Downloading (%d%%)
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml
index 28e9a816..907c6f47 100644
--- a/mastodon/src/main/res/values/styles.xml
+++ b/mastodon/src/main/res/values/styles.xml
@@ -38,7 +38,11 @@
- @style/Widget.Mastodon.PopupMenu
- @style/Widget.Mastodon.PopupMenu
- @style/Widget.Mastodon.M3.Switch
-
+ - true
+ - @color/m3_sys_light_primary
+ - @style/Widget.Mastodon.Toolbar.ActionMode
+ - @drawable/ic_actionmode_close
+
- @color/m3_sys_light_primary
- @color/m3_sys_light_on_primary
@@ -120,6 +124,10 @@
- false
- @style/Widget.Mastodon.PopupMenu
- @style/Widget.Mastodon.PopupMenu
+ - @style/Widget.Mastodon.M3.Switch
+ - true
+ - @color/m3_sys_dark_primary
+ - @drawable/ic_actionmode_close
- @color/m3_sys_dark_primary
@@ -170,12 +178,6 @@
- #FFF
-
-
@@ -189,6 +191,14 @@
- @color/action_bar_icons
+
+
+
+