diff --git a/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java b/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java new file mode 100644 index 000000000..524aaed83 --- /dev/null +++ b/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java @@ -0,0 +1,89 @@ +package org.joinmastodon.android.fragments; + +import static org.junit.Assert.*; + +import android.util.Pair; + +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.StatusContext; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +public class ThreadFragmentTest { + + private Status fakeStatus(String id, String inReplyTo) { + Status status = Status.ofFake(id, null, null); + status.inReplyToId = inReplyTo; + return status; + } + + private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) { + ThreadFragment.NeighborAncestryInfo info = new ThreadFragment.NeighborAncestryInfo(s); + info.descendantNeighbor = d; + info.ancestoringNeighbor = a; + return info; + } + + @Test + public void mapNeighborhoodAncestry() { + StatusContext context = new StatusContext(); + context.ancestors = List.of( + fakeStatus("oldest ancestor", null), + fakeStatus("younger ancestor", "oldest ancestor") + ); + Status mainStatus = fakeStatus("main status", "younger ancestor"); + context.descendants = List.of( + fakeStatus("first reply", "main status"), + fakeStatus("reply to first reply", "first reply"), + fakeStatus("third level reply", "reply to first reply"), + fakeStatus("another reply", "main status") + ); + + List neighbors = + ThreadFragment.mapNeighborhoodAncestry(mainStatus, context); + + assertEquals(List.of( + fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null), + fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)), + fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)), + fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus), + fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)), + fakeInfo(context.descendants.get(2), null, context.descendants.get(1)), + fakeInfo(context.descendants.get(3), null, null) + ), neighbors); + } + + @Test + public void sortStatusContext() { + StatusContext context = new StatusContext(); + context.ancestors = List.of( + fakeStatus("younger ancestor", "oldest ancestor"), + fakeStatus("oldest ancestor", null) + ); + context.descendants = List.of( + fakeStatus("reply to first reply", "first reply"), + fakeStatus("third level reply", "reply to first reply"), + fakeStatus("first reply", "main status"), + fakeStatus("another reply", "main status") + ); + + ThreadFragment.sortStatusContext( + fakeStatus("main status", "younger ancestor"), + context + ); + List expectedAncestors = List.of( + fakeStatus("oldest ancestor", null), + fakeStatus("younger ancestor", "oldest ancestor") + ); + List expectedDescendants = List.of( + fakeStatus("first reply", "main status"), + fakeStatus("reply to first reply", "first reply"), + fakeStatus("third level reply", "reply to first reply"), + fakeStatus("another reply", "main status") + ); + + // TODO: ??? i have no idea how this code works. it certainly doesn't return what i'd expect + } +} \ No newline at end of file diff --git a/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java b/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java index d99c200fe..be1657660 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java @@ -1,7 +1,6 @@ package org.joinmastodon.android; import android.app.Fragment; -import android.app.assist.AssistContent; import android.content.ClipData; import android.content.Intent; import android.net.Uri; @@ -19,6 +18,7 @@ import org.jsoup.internal.StringUtil; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import androidx.annotation.Nullable; import me.grishka.appkit.FragmentStackActivity; @@ -30,8 +30,8 @@ public class ExternalShareActivity extends FragmentStackActivity{ super.onCreate(savedInstanceState); if(savedInstanceState==null){ - String text = getIntent().getStringExtra(Intent.EXTRA_TEXT); - boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text); + Optional text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT)); + boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false); List sessions=AccountSessionManager.getInstance().getLoggedInAccounts(); if(sessions.isEmpty()){ @@ -40,11 +40,22 @@ public class ExternalShareActivity extends FragmentStackActivity{ }else if(sessions.size()==1 && !isMastodonURL){ openComposeFragment(sessions.get(0).getID()); }else{ - new AccountSwitcherSheet(this, false, false, isMastodonURL, accountSession -> { - if(accountSession!=null) - openComposeFragment(accountSession.getID()); - else - UiUtils.openURL(this, AccountSessionManager.getInstance().getLastActiveAccountID(), text); + new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> { + if (open && text.isPresent()) { + UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> { + if (clazz == null) { + finish(); + return; + } + args.putString("fromExternalShare", clazz.getSimpleName()); + Intent intent = new Intent(this, MainActivity.class); + intent.putExtras(args); + finish(); + startActivity(intent); + }); + } else { + openComposeFragment(accountId); + } }).show(); } } @@ -108,11 +119,4 @@ public class ExternalShareActivity extends FragmentStackActivity{ return null; return new ArrayList<>(l); } - - @Override - public void onProvideAssistContent(AssistContent outContent) { - super.onProvideAssistContent(outContent); - - outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain())); - } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index 7d3a45c2c..156f4a0a3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -88,6 +88,16 @@ public class GlobalUserPreferences{ catch (JsonSyntaxException ignored) { return orElse; } } + public static void removeAccount(String accountId) { + recentLanguages.remove(accountId); + pinnedTimelines.remove(accountId); + accountsInGlitchMode.remove(accountId); + accountsWithLocalOnlySupport.remove(accountId); + accountsWithContentTypesEnabled.remove(accountId); + accountsDefaultContentTypes.remove(accountId); + save(); + } + public static void load(){ SharedPreferences prefs=getPrefs(); playGifs=prefs.getBoolean("playGifs", true); @@ -218,4 +228,3 @@ public class GlobalUserPreferences{ DARK } } - diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 8371fbe95..f2aed9cdf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -9,6 +9,8 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.session.AccountSession; @@ -22,13 +24,13 @@ import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.updater.GithubSelfUpdater; +import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import androidx.annotation.Nullable; import me.grishka.appkit.FragmentStackActivity; -public class MainActivity extends FragmentStackActivity{ - +public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent { @Override protected void onCreate(@Nullable Bundle savedInstanceState){ UiUtils.setUserPreferredTheme(this); @@ -38,10 +40,18 @@ public class MainActivity extends FragmentStackActivity{ if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){ showFragmentClearingBackStack(new CustomWelcomeFragment()); }else{ - AccountSessionManager.getInstance().maybeUpdateLocalInfo(); AccountSession session; Bundle args=new Bundle(); Intent intent=getIntent(); + if(intent.hasExtra("fromExternalShare")) { + AccountSessionManager.getInstance() + .setLastActiveAccountID(intent.getStringExtra("account")); + AccountSessionManager.getInstance().maybeUpdateLocalInfo( + AccountSessionManager.getInstance().getLastActiveAccount()); + showFragmentForExternalShare(intent.getExtras()); + return; + } + boolean fromNotification = intent.getBooleanExtra("fromNotification", false); boolean hasNotification = intent.hasExtra("notification"); if(fromNotification){ @@ -55,6 +65,7 @@ public class MainActivity extends FragmentStackActivity{ }else{ session=AccountSessionManager.getInstance().getLastActiveAccount(); } + AccountSessionManager.getInstance().maybeUpdateLocalInfo(session); args.putString("account", session.getID()); Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment(); fragment.setArguments(args); @@ -78,12 +89,12 @@ public class MainActivity extends FragmentStackActivity{ @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); - if(intent.getBooleanExtra("fromNotification", false)){ + AccountSessionManager.getInstance().maybeUpdateLocalInfo(); + if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras()); + else if (intent.getBooleanExtra("fromNotification", false)) { String accountID=intent.getStringExtra("accountID"); - AccountSession accountSession; try{ - accountSession=AccountSessionManager.getInstance().getAccount(accountID); - DomainManager.getInstance().setCurrentDomain(accountSession.domain); + AccountSessionManager.getInstance().getAccount(accountID); }catch(IllegalStateException x){ return; } @@ -128,6 +139,19 @@ public class MainActivity extends FragmentStackActivity{ showFragment(fragment); } + private void showFragmentForExternalShare(Bundle args) { + String clazz = args.getString("fromExternalShare"); + Fragment fragment = switch (clazz) { + case "ThreadFragment" -> new ThreadFragment(); + case "ProfileFragment" -> new ProfileFragment(); + default -> null; + }; + if (fragment == null) return; + args.putBoolean("_can_go_back", true); + fragment.setArguments(args); + showFragment(fragment); + } + private void showCompose(){ AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount(); if(session==null || !session.activated) @@ -157,25 +181,40 @@ public class MainActivity extends FragmentStackActivity{ (fragmentContainers.get(fragmentContainers.size() - 1)).getId() ); Bundle currentArgs = currentFragment.getArguments(); - if (this.fragmentContainers.size() == 1 - && currentArgs != null - && currentArgs.getBoolean("_can_go_back", false) - && currentArgs.containsKey("account")) { + if (fragmentContainers.size() != 1 + || currentArgs == null + || !currentArgs.getBoolean("_can_go_back", false)) { + super.onBackPressed(); + return; + } + if (currentArgs.getBoolean("_finish_on_back", false)) { + finish(); + } else if (currentArgs.containsKey("account")) { Bundle args = new Bundle(); args.putString("account", currentArgs.getString("account")); - args.putString("tab", "notifications"); + if (getIntent().getBooleanExtra("fromNotification", false)) { + args.putString("tab", "notifications"); + } Fragment fragment=new HomeFragment(); fragment.setArguments(args); showFragmentClearingBackStack(fragment); - } else { - super.onBackPressed(); } } - @Override - public void onProvideAssistContent(AssistContent outContent) { - super.onProvideAssistContent(outContent); - outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain())); + public Fragment getCurrentFragment() { + for (int i = fragmentContainers.size() - 1; i >= 0; i--) { + FrameLayout fl = fragmentContainers.get(i); + if (fl.getVisibility() == View.VISIBLE) { + return getFragmentManager().findFragmentById(fl.getId()); + } + } + return null; } + @Override + public void onProvideAssistContent(AssistContent assistContent) { + super.onProvideAssistContent(assistContent); + Fragment fragment = getCurrentFragment(); + if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java index 616b0eb05..564a266bb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java +++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java @@ -29,6 +29,7 @@ import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.NotificationReceivedEvent; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.NotificationAction; import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.PushNotification; @@ -38,6 +39,7 @@ import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; @@ -57,7 +59,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{ private static final String ACTION_KEY_TEXT_REPLY = "ACTION_KEY_TEXT_REPLY"; private static final int SUMMARY_ID = 791; - private static int notificationId; + private static int notificationId = 0; @Override public void onReceive(Context context, Intent intent){ @@ -298,27 +300,60 @@ public class PushNotificationReceiver extends BroadcastReceiver{ } CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY); + // copied from ComposeFragment - TODO: generalize? + ArrayList mentions=new ArrayList<>(); + Status status = notification.status; + String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id; + if(!status.account.id.equals(ownID)) + mentions.add('@'+status.account.acct); + for(Mention mention:status.mentions){ + if(mention.id.equals(ownID)) + continue; + String m='@'+mention.acct; + if(!mentions.contains(m)) + mentions.add(m); + } + String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" "; + CreateStatus.Request req=new CreateStatus.Request(); - req.status = input.toString() + "\n\n" + "@" + notification.status.account.acct; - req.language = notification.status.language; - req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility); + req.status = initialText + input.toString(); + req.language = preferences.postingDefaultLanguage; + req.visibility = preferences.postingDefaultVisibility; req.inReplyToId = notification.status.id; if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){ req.spoilerText = "re: " + notification.status.spoilerText; } - new CreateStatus(req, UUID.randomUUID().toString()).exec(accountID); + new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() { + @Override + public void onSuccess(Status status) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ? + new Notification.Builder(context, accountID+"_"+notification.type) : + new Notification.Builder(context) + .setPriority(Notification.PRIORITY_DEFAULT) + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ? - new Notification.Builder(context, accountID+"_"+notification.type) : - new Notification.Builder(context) - .setPriority(Notification.PRIORITY_DEFAULT) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); + notification.status = status; + Intent contentIntent=new Intent(context, MainActivity.class); + contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + contentIntent.putExtra("fromNotification", true); + contentIntent.putExtra("accountID", accountID); + contentIntent.putExtra("notification", Parcels.wrap(notification)); - Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo) - .setContentText(context.getString(R.string.mo_notification_action_replied, notification.status.account.getDisplayUsername())) - .build(); - notificationManager.notify(accountID, notificationId, repliedNotification); + Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo) + .setContentTitle(context.getString(R.string.sk_notification_action_replied, notification.status.account.displayName)) + .setContentText(status.getStrippedText()) + .setCategory(Notification.CATEGORY_SOCIAL) + .setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + notificationManager.notify(accountID, notificationId, repliedNotification); + } + + @Override + public void onError(ErrorResponse errorResponse) { + + } + }).exec(accountID); } -} \ No newline at end of file +} 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 e4e54b451..335d00914 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Notification; -import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.utils.StatusFilterPredicate; @@ -160,7 +159,7 @@ public class CacheController{ } } Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); - new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null) + new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma()) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/PleromaMarkNotificationsRead.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/PleromaMarkNotificationsRead.java new file mode 100644 index 000000000..6e9fe23e7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/PleromaMarkNotificationsRead.java @@ -0,0 +1,30 @@ +package org.joinmastodon.android.api.requests.notifications; + +import android.text.TextUtils; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Notification; + +import java.util.List; + +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class PleromaMarkNotificationsRead extends MastodonAPIRequest> { + private String maxID; + public PleromaMarkNotificationsRead(String maxID) { + super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){}); + this.maxID = maxID; + } + + @Override + public RequestBody getRequestBody() { + MultipartBody.Builder builder=new MultipartBody.Builder() + .setType(MultipartBody.FORM); + if(!TextUtils.isEmpty(maxID)) + builder.addFormDataPart("max_id", maxID); + return builder.build(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetBubbleTimeline.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetBubbleTimeline.java new file mode 100644 index 000000000..9b54d1895 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetBubbleTimeline.java @@ -0,0 +1,23 @@ +package org.joinmastodon.android.api.requests.timelines; + +import android.text.TextUtils; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +import java.util.List; + +public class GetBubbleTimeline extends MastodonAPIRequest> { + public GetBubbleTimeline(String maxID, int limit) { + super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){}); + if(!TextUtils.isEmpty(maxID)) + addQueryParameter("max_id", maxID); + if(limit>0) + addQueryParameter("limit", limit+""); + if(GlobalUserPreferences.replyVisibility != null) + addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHashtagTimeline.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHashtagTimeline.java index 4a3c831df..1670c38c2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHashtagTimeline.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHashtagTimeline.java @@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines; import com.google.gson.reflect.TypeToken; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Status; @@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest>{ addQueryParameter("min_id", minID); if(limit>0) addQueryParameter("limit", ""+limit); + if(GlobalUserPreferences.replyVisibility != null) + addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetListTimeline.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetListTimeline.java index 145a740bc..82d537971 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetListTimeline.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetListTimeline.java @@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines; import com.google.gson.reflect.TypeToken; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Status; @@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest> { addQueryParameter("limit", ""+limit); if(sinceID!=null) addQueryParameter("since_id", sinceID); + if(GlobalUserPreferences.replyVisibility != null) + addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java index 6723c18b9..7ec562704 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java @@ -4,6 +4,7 @@ import android.text.TextUtils; import com.google.gson.reflect.TypeToken; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Status; @@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest>{ addQueryParameter("max_id", maxID); if(limit>0) addQueryParameter("limit", limit+""); + if(GlobalUserPreferences.replyVisibility != null) + addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility); } } 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 da32d9d1a..30acb30d6 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,7 @@ package org.joinmastodon.android.api.session; +import android.net.Uri; + import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.PushSubscriptionManager; @@ -7,6 +9,7 @@ import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Markers; import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.PushSubscription; @@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token; import java.util.ArrayList; import java.util.List; +import java.util.Optional; public class AccountSession{ public Token token; @@ -87,4 +91,15 @@ public class AccountSession{ pushSubscriptionManager=new PushSubscriptionManager(getID()); return pushSubscriptionManager; } + + public Optional getInstance() { + return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain)); + } + + public Uri getInstanceUri() { + return new Uri.Builder() + .scheme("https") + .authority(getInstance().map(i -> i.normalizedUri).orElse(domain)) + .build(); + } } 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 00ed9cb0c..6f90a5c73 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 @@ -15,6 +15,7 @@ import android.util.Log; 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; @@ -121,6 +122,12 @@ public class AccountSessionManager{ sessions.put(session.getID(), session); lastActiveAccountID=session.getID(); writeAccountsFile(); + + // write initial instance info to file immediately to avoid sessions without instance info + InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper(); + wrapper.instance = instance; + MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri)); + updateMoreInstanceInfo(instance, instance.uri); if(PushSubscriptionManager.arePushNotificationsAvailable()){ session.getPushSubscriptionManager().registerAccountForPush(null); @@ -129,14 +136,16 @@ public class AccountSessionManager{ } public synchronized void writeAccountsFile(){ - File file=new File(MastodonApp.context.getFilesDir(), "accounts.json"); + File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~"); + File file = new File(MastodonApp.context.getFilesDir(), "accounts.json"); try{ - try(FileOutputStream out=new FileOutputStream(file)){ + try(FileOutputStream out=new FileOutputStream(tmpFile)){ SessionsStorageWrapper w=new SessionsStorageWrapper(); w.accounts=new ArrayList<>(sessions.values()); OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); MastodonAPIController.gson.toJson(w, writer); writer.flush(); + if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath()); } }catch(IOException x){ Log.e(TAG, "Error writing accounts file", x); @@ -189,6 +198,7 @@ public class AccountSessionManager{ AccountSession session=getAccount(id); session.getCacheController().closeDatabase(); MastodonApp.context.deleteDatabase(id+".db"); + GlobalUserPreferences.removeAccount(id); sessions.remove(id); if(lastActiveAccountID.equals(id)){ if(sessions.isEmpty()) @@ -259,31 +269,35 @@ public class AccountSessionManager{ } public void maybeUpdateLocalInfo(){ + maybeUpdateLocalInfo(null); + } + + public void maybeUpdateLocalInfo(AccountSession activeSession){ long now=System.currentTimeMillis(); HashSet domains=new HashSet<>(); for(AccountSession session:sessions.values()){ domains.add(session.domain.toLowerCase()); -// if(now-session.infoLastUpdated>24L*3600_000L){ - updateSessionPreferences(session); - updateSessionLocalInfo(session); -// } -// if(now-session.filtersLastUpdated>3600_000L){ - updateSessionWordFilters(session); -// } + if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){ + updateSessionPreferences(session); + updateSessionLocalInfo(session); + } + if(now-session.filtersLastUpdated>3600_000L || session == activeSession){ + updateSessionWordFilters(session); + } updateSessionMarkers(session); } if(loadedInstances){ - maybeUpdateCustomEmojis(domains); + maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null); } } - private void maybeUpdateCustomEmojis(Set domains){ + private void maybeUpdateCustomEmojis(Set domains, String activeDomain){ long now=System.currentTimeMillis(); for(String domain:domains){ -// Long lastUpdated=instancesLastUpdated.get(domain); -// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){ - updateInstanceInfo(domain); -// } + Long lastUpdated=instancesLastUpdated.get(domain); + if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){ + updateInstanceInfo(domain); + } } } @@ -411,7 +425,9 @@ public class AccountSessionManager{ @Override public void onError(ErrorResponse error){ - + InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper(); + wrapper.instance = instance; + MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain)); } }) .execNoAuth(domain); @@ -422,10 +438,13 @@ public class AccountSessionManager{ } private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){ - try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){ + File file = getInstanceInfoFile(domain); + File tmpFile = new File(file.getPath() + "~"); + try(FileOutputStream out=new FileOutputStream(tmpFile)){ OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8); MastodonAPIController.gson.toJson(emojis, writer); writer.flush(); + if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath()); }catch(IOException x){ Log.w(TAG, "Error writing instance info file for "+domain, x); } @@ -445,7 +464,7 @@ public class AccountSessionManager{ } if(!loadedInstances){ loadedInstances=true; - maybeUpdateCustomEmojis(domains); + maybeUpdateCustomEmojis(domains, null); } } @@ -469,10 +488,6 @@ public class AccountSessionManager{ return instances.get(domain); } - public Instance getInstanceInfoForAccount(String account) { - return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain); - } - public void updateAccountInfo(String id, Account account){ AccountSession session=getAccount(id); session.self=account; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java index 424cea88c..9ddf968b6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -131,4 +132,13 @@ public class AccountTimelineFragment extends StatusListFragment{ protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.ACCOUNT; } + + @Override + public Uri getWebUri(Uri.Builder base) { + // could return different uris based on filter (e.g. media -> "/media"), but i want to + // return the remote url to the user, and i don't know whether i'd need to append + // '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything + // about the remote instance. so, just returning the base url to the user instead + return Uri.parse(user.url); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java index a87b8c465..1b6b8ce2a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AnnouncementsFragment.java @@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments; import static java.util.stream.Collectors.toList; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.View; @@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment }) .exec(accountID); } + + @Override + public Uri getWebUri(Uri.Builder base) { + return isInstanceAkkoma() ? base.path("/announcements").build() : null; + } } 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 0d25daa52..3c6288693 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.app.assist.AssistContent; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Paint; @@ -15,6 +16,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.animation.TranslateAnimation; import android.widget.ImageButton; @@ -49,6 +51,7 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.TypedObjectPool; import java.util.ArrayList; @@ -69,7 +72,7 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public abstract class BaseStatusListFragment extends BaseRecyclerFragment implements PhotoViewerHost, ScrollableToTop, HasFab, DomainDisplay{ +public abstract class BaseStatusListFragment extends RecyclerFragment implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri, DomainDisplay { protected ArrayList displayItems=new ArrayList<>(); protected DisplayItemsAdapter adapter; protected String accountID; @@ -132,7 +135,7 @@ public abstract class BaseStatusListFragment exten displayItems.clear(); } - protected void prependItems(List items, boolean notify){ + protected int prependItems(List items, boolean notify){ data.addAll(0, items); int offset=0; for(T s:items){ @@ -145,6 +148,7 @@ public abstract class BaseStatusListFragment exten } if(notify) adapter.notifyItemRangeInserted(0, offset); + return offset; } protected String getMaxID(){ @@ -205,7 +209,7 @@ public abstract class BaseStatusListFragment exten @Override public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ MediaAttachmentViewController holder=findPhotoViewHolder(index); - if(holder!=null){ + if(holder!=null && list!=null){ transitioningHolder=holder; View view=transitioningHolder.photo; int[] pos={0, 0}; @@ -337,6 +341,8 @@ public abstract class BaseStatusListFragment exten private Rect tmpRect=new Rect(); @Override public void getSelectorBounds(View view, Rect outRect){ + boolean hasDescendant = false, hasAncestor = false, isWarning = false; + int lastIndex = -1, firstIndex = -1; list.getDecoratedBoundsWithMargins(view, outRect); RecyclerView.ViewHolder holder=list.getChildViewHolder(view); if(holder instanceof StatusDisplayItem.Holder){ @@ -348,18 +354,40 @@ public abstract class BaseStatusListFragment exten for(int i=0;i h){ String otherID=((StatusDisplayItem.Holder) holder).getItemID(); if(otherID.equals(id)){ + if (firstIndex < 0) firstIndex = i; + lastIndex = i; + StatusDisplayItem item = h.getItem(); + hasDescendant = item.hasDescendantNeighbor; + // no for direct descendants because main status (right above) is + // being displayed with an extended footer - no connected layout + hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant; list.getDecoratedBoundsWithMargins(child, tmpRect); outRect.left=Math.min(outRect.left, tmpRect.left); outRect.top=Math.min(outRect.top, tmpRect.top); outRect.right=Math.max(outRect.right, tmpRect.right); outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom); + if (holder instanceof WarningFilteredStatusDisplayItem.Holder) { + isWarning = true; + } } } } } + // shifting the selection box down + // see also: FooterStatusDisplayItem#onBind (setMargins) + if (isWarning || firstIndex < 0 || lastIndex < 0) return; + int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1; + boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() && + list.getChildViewHolder(list.getChildAt(prevIndex)) + instanceof WarningFilteredStatusDisplayItem.Holder; + boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() && + list.getChildViewHolder(list.getChildAt(nextIndex)) + instanceof WarningFilteredStatusDisplayItem.Holder; + if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4); + if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4); } }); list.setItemAnimator(new BetterItemAnimator()); @@ -568,6 +596,14 @@ public abstract class BaseStatusListFragment exten } } + public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) { + holder.rebind(); + MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class); + if(mediaGrid!=null){ + adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition()); + } + } + public void onGapClick(GapStatusDisplayItem.Holder item){} public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){ @@ -579,6 +615,7 @@ public abstract class BaseStatusListFragment exten warning.getItem().status.filterRevealed = true; } + @Override public String getAccountID(){ return accountID; } @@ -717,6 +754,10 @@ public abstract class BaseStatusListFragment exten return attachmentViewsPool; } + @Override + public void onProvideAssistContent(AssistContent assistContent) { + assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon())); + } protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ @@ -778,6 +819,7 @@ public abstract class BaseStatusListFragment exten RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling); if(holder instanceof StatusDisplayItem.Holder ih && siblingHolder instanceof StatusDisplayItem.Holder sh && (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){ + if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue; drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java index 6f863bdd2..c69967435 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses; @@ -41,4 +42,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{ protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.ACCOUNT; } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path("/bookmarks").build(); + } } 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 7275b412e..db75e9835 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -252,10 +252,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr accountID=getArguments().getString("account"); contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID); - if (contentType == null && GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) { - // if formatting is enabled, use plain to avoid confusing unspecified default setting - contentType = ContentType.PLAIN; - } AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); self=session.self; @@ -274,9 +270,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr Nav.finish(this); return; } - if(customEmojis.isEmpty()){ - AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain); - } Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments(); if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus")); @@ -1146,7 +1139,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } req.status=text; req.localOnly=localOnly; - req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility; + req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility; req.sensitive=sensitive; req.language=language; req.contentType=contentType; @@ -1800,11 +1793,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr pollChanged=true; updatePublishButtonState(); })); - option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)}); + + int maxCharactersPerOption = 50; + if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0) + maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption; + else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0) + maxCharactersPerOption = instance.pollLimits.maxOptionChars; + option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)}); pollOptionsView.addView(option.view); pollOptions.add(option); - if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4)) + + int maxPollOptions = 4; + if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0) + maxPollOptions = instance.configuration.polls.maxOptions; + else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0) + maxPollOptions = instance.pollLimits.maxOptions; + + if(pollOptions.size()==maxPollOptions) addPollOptionBtn.setVisibility(View.GONE); return option; } @@ -1961,7 +1967,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr Menu m=visibilityPopup.getMenu(); MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only); boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID); - if (instance.pleroma != null) { + if (instance.isAkkoma()) { m.findItem(R.id.vis_local).setVisible(true); } else if (localOnly || prefsSaysSupported) { localOnlyItem.setVisible(true); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java index 8d986b95e..ee751c3ce 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java @@ -32,9 +32,12 @@ import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.lists.GetLists; import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags; +import org.joinmastodon.android.api.session.AccountSession; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.CustomLocalTimeline; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.HeaderPaginationList; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.ui.DividerItemDecoration; @@ -196,7 +199,7 @@ public class EditTimelinesFragment extends RecyclerFragment makeBackItem(listsMenu); makeBackItem(hashtagsMenu); - TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu)); + TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu)); listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu)); hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu)); @@ -222,7 +225,7 @@ public class EditTimelinesFragment extends RecyclerFragment @Override protected void doLoadData(int offset, int count){ - onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false); + onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false); updateOptionsMenu(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java index 41e6e0c7f..e649bc7ba 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses; @@ -41,4 +42,11 @@ public class FavoritedStatusListFragment extends StatusListFragment{ protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.ACCOUNT; } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.encodedPath(isInstanceAkkoma() + ? '/' + getSession().self.username + "#favorites" + : "/favourites").build(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java index 9cd86da83..9f144634d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.graphics.Rect; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.ProgressBarButton; +import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import java.util.Collections; @@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class FollowRequestsListFragment extends RecyclerFragment implements ScrollableToTop{ +public class FollowRequestsListFragment extends RecyclerFragment implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { private String accountID; private Map relationships=Collections.emptyMap(); private GetAccountRelationships relationshipsRequest; @@ -149,8 +151,13 @@ public class FollowRequestsListFragment extends RecyclerFragment implements ImageLoaderRecyclerAdapter{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java index ded699f99..e2e6ae59c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java @@ -1,5 +1,6 @@ package org.joinmastodon.android.fragments; +import android.net.Uri; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; @@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.utils.ProvidesAssistContent; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.views.UsableRecyclerView; -public class FollowedHashtagsFragment extends RecyclerFragment implements ScrollableToTop { +public class FollowedHashtagsFragment extends RecyclerFragment implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { private String nextMaxID; - private String accountId; + private String accountID; public FollowedHashtagsFragment() { super(20); @@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment implemen public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args=getArguments(); - accountId=args.getString("account"); + accountID=args.getString("account"); setTitle(R.string.sk_hashtags_you_follow); } @@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment implemen onDataLoaded(result, nextMaxID!=null); } }) - .exec(accountId); + .exec(accountID); } @Override @@ -76,8 +78,13 @@ public class FollowedHashtagsFragment extends RecyclerFragment implemen } @Override - public boolean isScrolledToTop() { - return list.getChildAt(0).getTop() == 0; + public String getAccountID() { + return accountID; + } + + @Override + public Uri getWebUri(Uri.Builder base) { + return isInstanceAkkoma() ? null : base.path("/followed_tags").build(); } private class HashtagsAdapter extends RecyclerView.Adapter{ @@ -114,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment implemen @Override public void onClick() { - UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following); + UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java new file mode 100644 index 000000000..67a3bbfac --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HasAccountID.java @@ -0,0 +1,23 @@ +package org.joinmastodon.android.fragments; + +import org.joinmastodon.android.api.session.AccountSession; +import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.model.Instance; + +import java.util.Optional; + +public interface HasAccountID { + String getAccountID(); + + default AccountSession getSession() { + return AccountSessionManager.getInstance().getAccount(getAccountID()); + } + + default boolean isInstanceAkkoma() { + return getInstance().map(Instance::isAkkoma).orElse(false); + } + + default Optional getInstance() { + return getSession().getInstance(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java index c7ce498ae..5fec68deb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.view.HapticFeedbackConstants; import android.view.Menu; @@ -8,7 +9,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; import android.widget.Toast; import org.joinmastodon.android.DomainManager; @@ -167,4 +167,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.PUBLIC; } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build(); + } } 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 2424a8abc..8e19ad402 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments; import android.app.Fragment; import android.app.NotificationManager; +import android.app.assist.AssistContent; import android.content.Intent; import android.graphics.Outline; import android.os.Build; @@ -40,16 +41,13 @@ import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.TabBar; +import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; - -import androidx.annotation.IdRes; -import androidx.annotation.Nullable; - -import com.squareup.otto.Subscribe; +import java.util.Optional; import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.Nav; @@ -63,11 +61,9 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class HomeFragment extends AppKitFragment implements OnBackPressedListener{ +public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID { private FragmentRootLinearLayout content; - private HomeTabFragment homeTabFragment; - private NotificationsFragment notificationsFragment; private DiscoverFragment searchFragment; private ProfileFragment profileFragment; @@ -79,6 +75,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene private int currentTab=R.id.tab_home; private String accountID; + private boolean isPleroma; @Override public void onCreate(Bundle savedInstanceState){ @@ -86,18 +83,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene E.register(this); accountID=getArguments().getString("account"); setTitle(R.string.mo_app_name); + isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance() + .map(Instance::isAkkoma) + .orElse(false); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) setRetainInstance(true); + // TODO: clean up if(savedInstanceState==null){ Bundle args=new Bundle(); args.putString("account", accountID); - homeTabFragment=new HomeTabFragment(); homeTabFragment.setArguments(args); - args=new Bundle(args); + args.putBoolean("disableDiscover", isPleroma); args.putBoolean("noAutoLoad", true); searchFragment=new DiscoverFragment(); searchFragment.setArguments(args); @@ -149,7 +149,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene .add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment) .commit(); - String defaultTab=getArguments().getString("tab"); if("notifications".equals(defaultTab)){ tabBar.selectTab(R.id.tab_notifications); @@ -170,19 +169,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene @Override public void onViewStateRestored(Bundle savedInstanceState){ super.onViewStateRestored(savedInstanceState); - if(savedInstanceState==null) return; - - homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment"); - searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment"); notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment"); currentTab=savedInstanceState.getInt("selectedTab"); tabBar.selectTab(currentTab); Fragment current=fragmentForTab(currentTab); - getChildFragmentManager().beginTransaction() .hide(homeTabFragment) .hide(searchFragment) @@ -190,15 +184,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene .hide(profileFragment) .show(current) .commit(); - maybeTriggerLoading(current); } @Override public void onHiddenChanged(boolean hidden){ super.onHiddenChanged(hidden); - if (!hidden && fragmentForTab(currentTab) instanceof DomainDisplay display) - DomainManager.getInstance().setCurrentDomain(display.getDomain()); fragmentForTab(currentTab).onHiddenChanged(hidden); } @@ -222,9 +213,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); } WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0); - homeTabFragment.onApplyWindowInsets(topOnlyInsets); - searchFragment.onApplyWindowInsets(topOnlyInsets); notificationsFragment.onApplyWindowInsets(topOnlyInsets); profileFragment.onApplyWindowInsets(topOnlyInsets); @@ -243,34 +232,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene throw new IllegalArgumentException(); } + public void setCurrentTab(@IdRes int tab){ + if(tab==currentTab) + return; + tabBar.selectTab(tab); + onTabSelected(tab); + } + private void onTabSelected(@IdRes int tab){ Fragment newFragment=fragmentForTab(tab); if(tab==currentTab){ - if(tab == R.id.tab_search){ - if(newFragment instanceof ScrollableToTop scrollable) - scrollable.scrollToTop(); - searchFragment.selectSearch(); - return; - } - if(newFragment instanceof ScrollableToTop scrollable) + if (tab == R.id.tab_search) + searchFragment.onSelect(); + else if(newFragment instanceof ScrollableToTop scrollable) scrollable.scrollToTop(); return; } - if(tab==currentTab && tab == R.id.tab_search){ - if(newFragment instanceof ScrollableToTop scrollable) - scrollable.scrollToTop(); - return; - } - - if (newFragment instanceof DomainDisplay display) { - DomainManager.getInstance().setCurrentDomain(display.getDomain()); - } - getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit(); maybeTriggerLoading(newFragment); if (newFragment instanceof HasFab fabulous) fabulous.showFab(); currentTab=tab; ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); + if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch(); } private void maybeTriggerLoading(Fragment newFragment){ @@ -297,10 +280,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")"); } - new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> { - getActivity().finish(); - getActivity().startActivity(new Intent(getActivity(), MainActivity.class)); - }).show(); + new AccountSwitcherSheet(getActivity(), this).show(); return true; } if(tab==R.id.tab_search){ @@ -336,7 +316,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene public void onSaveInstanceState(Bundle outState){ super.onSaveInstanceState(outState); outState.putInt("selectedTab", currentTab); - if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment); if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment); if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); @@ -345,10 +324,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene public void updateNotificationBadge() { AccountSession session = AccountSessionManager.getInstance().getAccount(accountID); - Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain); - if (instance == null) return; + Optional instance = session.getInstance(); + if (instance.isEmpty()) return; // avoiding incompatibility with akkoma - new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null) + new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma()) .setCallback(new Callback<>() { @Override public void onSuccess(List notifications) { @@ -356,9 +335,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene try { long newestId = Long.parseLong(notifications.get(0).id); long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId); - System.out.println("NEWEST: " + newestId); - System.out.println("LAST SEEN: " + lastSeenId); - setNotificationBadge(newestId > lastSeenId); } catch (Exception ignored) { setNotificationBadge(false); @@ -372,6 +348,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene } }).exec(accountID); } + public void setNotificationBadge(boolean badge) { notificationTabIcon.setImageResource(badge ? R.drawable.ic_fluent_alert_28_selector_badged @@ -387,4 +364,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) { setNotificationBadge(false); } + + @Override + public String getAccountID() { + return accountID; + } + + @Override + public void onProvideAssistContent(AssistContent assistContent) { + callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java index 6994b4d84..57df424da 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -10,6 +10,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; +import android.app.assist.AssistContent; import android.content.Context; import android.os.Build; import android.os.Bundle; @@ -56,6 +57,7 @@ import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.updater.GithubSelfUpdater; +import org.joinmastodon.android.utils.ProvidesAssistContent; import java.util.Collection; import java.util.HashMap; @@ -73,7 +75,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; -public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay, HasFab { +public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent { private static final int ANNOUNCEMENTS_RESULT = 654; private String accountID; @@ -108,7 +110,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab super.onCreate(savedInstanceState); E.register(this); accountID = getArguments().getString("account"); - timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES); + timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)); assert timelineDefinitions != null; if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE); count = timelineDefinitions.size(); @@ -209,10 +211,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab if (fragments[position] instanceof BaseRecyclerFragment page){ if(!page.loaded && !page.isDataLoading()) page.loadData(); } - - //update recent app list url - if (fragments[position] instanceof DomainDisplay page) - DomainManager.getInstance().setCurrentDomain(page.getDomain()); } }); @@ -297,14 +295,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab }).exec(accountID); } - @Override - public String getDomain() { - if (fragments[pager.getCurrentItem()] instanceof DomainDisplay page) { - return page.getDomain(); - } - return DomainDisplay.super.getDomain(); - } - private void onFabClick(View v){ if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment l) { l.onFabClick(v); @@ -722,6 +712,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab return fab; } + @Override + public void onProvideAssistContent(AssistContent assistContent) { + callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent); + } + private class HomePagerAdapter extends RecyclerView.Adapter { @NonNull @Override 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 5fb3a9cc9..afb70884d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -291,4 +292,9 @@ public class HomeTimelineFragment extends StatusListFragment { protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.HOME; } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path("/").build(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java index da6322a85..0e03a19e6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java @@ -1,13 +1,13 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageButton; import androidx.annotation.Nullable; @@ -168,4 +168,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment { protected Filter.FilterContext getFilterContext() { return Filter.FilterContext.HOME; } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path("/lists/" + listID).build(); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java deleted file mode 100644 index 3655bd3ee..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java +++ /dev/null @@ -1,263 +0,0 @@ -package org.joinmastodon.android.fragments; - -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.squareup.otto.Subscribe; - -import org.joinmastodon.android.E; -import org.joinmastodon.android.R; -import org.joinmastodon.android.api.MastodonAPIRequest; -import org.joinmastodon.android.api.requests.lists.AddAccountsToList; -import org.joinmastodon.android.api.requests.lists.CreateList; -import org.joinmastodon.android.api.requests.lists.GetLists; -import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList; -import org.joinmastodon.android.events.ListDeletedEvent; -import org.joinmastodon.android.events.ListUpdatedCreatedEvent; -import org.joinmastodon.android.model.ListTimeline; -import org.joinmastodon.android.ui.DividerItemDecoration; -import org.joinmastodon.android.ui.M3AlertDialogBuilder; -import org.joinmastodon.android.ui.views.ListTimelineEditor; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import me.grishka.appkit.Nav; -import me.grishka.appkit.api.Callback; -import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.utils.BindableViewHolder; -import me.grishka.appkit.views.UsableRecyclerView; - -public class ListTimelinesFragment extends RecyclerFragment implements ScrollableToTop { - private String accountId; - private String profileAccountId; - private final HashMap userInListBefore = new HashMap<>(); - private final HashMap userInList = new HashMap<>(); - private ListsAdapter adapter; - - public ListTimelinesFragment() { - super(10); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle args=getArguments(); - accountId=args.getString("account"); - setHasOptionsMenu(true); - E.register(this); - - if(args.containsKey("profileAccount")){ - profileAccountId=args.getString("profileAccount"); - String profileDisplayUsername = args.getString("profileDisplayUsername"); - setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername)); - } else { - setTitle(R.string.sk_your_lists); - } - } - - @Override - protected void onShown(){ - super.onShown(); - if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) - loadData(); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_list, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.create) { - ListTimelineEditor editor = new ListTimelineEditor(getContext()); - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.sk_create_list_title) - .setIcon(R.drawable.ic_fluent_people_add_28_regular) - .setView(editor) - .setPositiveButton(R.string.sk_create, (d, which) -> - new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() { - @Override - public void onSuccess(ListTimeline list) { - data.add(0, list); - adapter.notifyItemRangeInserted(0, 1); - E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy)); - } - - @Override - public void onError(ErrorResponse error) { - error.showToast(getContext()); - } - }).exec(accountId) - ) - .setNegativeButton(R.string.cancel, (d, which) -> {}) - .show(); - } - return true; - } - - private void saveListMembership(String listId, boolean isMember) { - userInList.put(listId, isMember); - List accountIdList = Collections.singletonList(profileAccountId); - MastodonAPIRequest req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList); - req.setCallback(new Callback<>() { - @Override - public void onSuccess(Object o) {} - - @Override - public void onError(ErrorResponse error) { - error.showToast(getContext()); - } - }).exec(accountId); - } - - @Override - protected void doLoadData(int offset, int count){ - userInListBefore.clear(); - userInList.clear(); - currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists()) - .setCallback(new SimpleCallback<>(this) { - @Override - public void onSuccess(List lists) { - if (getActivity() == null) return; - for (ListTimeline l : lists) userInListBefore.put(l.id, true); - userInList.putAll(userInListBefore); - if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false); - if (profileAccountId == null) return; - - currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) { - @Override - public void onSuccess(List allLists) { - if (getActivity() == null) return; - List newLists = new ArrayList<>(); - for (ListTimeline l : allLists) { - if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l); - if (!userInListBefore.containsKey(l.id)) { - userInListBefore.put(l.id, false); - } - } - userInList.putAll(userInListBefore); - onDataLoaded(newLists, false); - } - }).exec(accountId); - } - }) - .exec(accountId); - } - - @Subscribe - public void onListDeletedEvent(ListDeletedEvent event) { - for (int i = 0; i < data.size(); i++) { - ListTimeline item = data.get(i); - if (item.id.equals(event.id)) { - data.remove(i); - adapter.notifyItemRemoved(i); - break; - } - } - } - - @Subscribe - public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) { - for (int i = 0; i < data.size(); i++) { - ListTimeline item = data.get(i); - if (item.id.equals(event.id)) { - item.title = event.title; - item.repliesPolicy = event.repliesPolicy; - adapter.notifyItemChanged(i); - break; - } - } - } - - @Override - protected RecyclerView.Adapter getAdapter() { - return adapter = new ListsAdapter(); - } - - @Override - public void scrollToTop() { - smoothScrollRecyclerViewToTop(list); - } - - @Override - public boolean isScrolledToTop() { - return list.getChildAt(0).getTop() == 0; - } - - private class ListsAdapter extends RecyclerView.Adapter{ - @NonNull - @Override - public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new ListViewHolder(); - } - - @Override - public void onBindViewHolder(@NonNull ListViewHolder holder, int position) { - holder.bind(data.get(position)); - } - - @Override - public int getItemCount() { - return data.size(); - } - } - - private class ListViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ - private final TextView title; - private final CheckBox listToggle; - - public ListViewHolder(){ - super(getActivity(), R.layout.item_text, list); - title=findViewById(R.id.title); - listToggle=findViewById(R.id.list_toggle); - } - - @Override - public void onBind(ListTimeline item) { - title.setText(item.title); - title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null); - if (profileAccountId != null) { - Boolean checked = userInList.get(item.id); - listToggle.setVisibility(View.VISIBLE); - listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked); - listToggle.setOnClickListener(this::onClickToggle); - } else { - listToggle.setVisibility(View.GONE); - } - } - - private void onClickToggle(View view) { - saveListMembership(item.id, listToggle.isChecked()); - } - - @Override - public void onClick() { - Bundle args=new Bundle(); - args.putString("account", accountId); - args.putString("listID", item.id); - args.putString("listTitle", item.title); - if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal()); - Nav.go(getActivity(), ListTimelineFragment.class, args); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListsFragment.java new file mode 100644 index 000000000..243a104c6 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListsFragment.java @@ -0,0 +1,270 @@ +package org.joinmastodon.android.fragments; + +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.squareup.otto.Subscribe; + +import org.joinmastodon.android.E; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.api.requests.lists.AddAccountsToList; +import org.joinmastodon.android.api.requests.lists.CreateList; +import org.joinmastodon.android.api.requests.lists.GetLists; +import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList; +import org.joinmastodon.android.events.ListDeletedEvent; +import org.joinmastodon.android.events.ListUpdatedCreatedEvent; +import org.joinmastodon.android.model.ListTimeline; +import org.joinmastodon.android.ui.DividerItemDecoration; +import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.views.ListTimelineEditor; +import org.joinmastodon.android.utils.ProvidesAssistContent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.views.UsableRecyclerView; + +public class ListsFragment extends RecyclerFragment implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri { + private String accountID; + private String profileAccountId; + private final HashMap userInListBefore = new HashMap<>(); + private final HashMap userInList = new HashMap<>(); + private ListsAdapter adapter; + + public ListsFragment() { + super(10); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle args = getArguments(); + accountID = args.getString("account"); + setHasOptionsMenu(true); + E.register(this); + + if(args.containsKey("profileAccount")){ + profileAccountId=args.getString("profileAccount"); + String profileDisplayUsername = args.getString("profileDisplayUsername"); + setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername)); + } else { + setTitle(R.string.sk_your_lists); + } + } + + @Override + protected void onShown(){ + super.onShown(); + if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) + loadData(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_list, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.create) { + ListTimelineEditor editor = new ListTimelineEditor(getContext()); + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.sk_create_list_title) + .setIcon(R.drawable.ic_fluent_people_add_28_regular) + .setView(editor) + .setPositiveButton(R.string.sk_create, (d, which) -> + new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() { + @Override + public void onSuccess(ListTimeline list) { + data.add(0, list); + adapter.notifyItemRangeInserted(0, 1); + E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy)); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getContext()); + } + }).exec(accountID) + ) + .setNegativeButton(R.string.cancel, (d, which) -> {}) + .show(); + } + return true; + } + + private void saveListMembership(String listId, boolean isMember) { + userInList.put(listId, isMember); + List accountIdList = Collections.singletonList(profileAccountId); + MastodonAPIRequest req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList); + req.setCallback(new Callback<>() { + @Override + public void onSuccess(Object o) {} + + @Override + public void onError(ErrorResponse error) { + error.showToast(getContext()); + } + }).exec(accountID); + } + + @Override + protected void doLoadData(int offset, int count){ + userInListBefore.clear(); + userInList.clear(); + currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists()) + .setCallback(new SimpleCallback<>(this) { + @Override + public void onSuccess(List lists) { + if (getActivity() == null) return; + for (ListTimeline l : lists) userInListBefore.put(l.id, true); + userInList.putAll(userInListBefore); + if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false); + if (profileAccountId == null) return; + + currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) { + @Override + public void onSuccess(List allLists) { + if (getActivity() == null) return; + List newLists = new ArrayList<>(); + for (ListTimeline l : allLists) { + if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l); + if (!userInListBefore.containsKey(l.id)) { + userInListBefore.put(l.id, false); + } + } + userInList.putAll(userInListBefore); + onDataLoaded(newLists, false); + } + }).exec(accountID); + } + }) + .exec(accountID); + } + + @Subscribe + public void onListDeletedEvent(ListDeletedEvent event) { + for (int i = 0; i < data.size(); i++) { + ListTimeline item = data.get(i); + if (item.id.equals(event.id)) { + data.remove(i); + adapter.notifyItemRemoved(i); + break; + } + } + } + + @Subscribe + public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) { + for (int i = 0; i < data.size(); i++) { + ListTimeline item = data.get(i); + if (item.id.equals(event.id)) { + item.title = event.title; + item.repliesPolicy = event.repliesPolicy; + adapter.notifyItemChanged(i); + break; + } + } + } + + @Override + protected RecyclerView.Adapter getAdapter() { + return adapter = new ListsAdapter(); + } + + @Override + public void scrollToTop() { + smoothScrollRecyclerViewToTop(list); + } + + @Override + public String getAccountID() { + return accountID; + } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path("/lists").build(); + } + + private class ListsAdapter extends RecyclerView.Adapter{ + @NonNull + @Override + public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new ListViewHolder(); + } + + @Override + public void onBindViewHolder(@NonNull ListViewHolder holder, int position) { + holder.bind(data.get(position)); + } + + @Override + public int getItemCount() { + return data.size(); + } + } + + private class ListViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ + private final TextView title; + private final CheckBox listToggle; + + public ListViewHolder(){ + super(getActivity(), R.layout.item_text, list); + title=findViewById(R.id.title); + listToggle=findViewById(R.id.list_toggle); + } + + @Override + public void onBind(ListTimeline item) { + title.setText(item.title); + title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null); + if (profileAccountId != null) { + Boolean checked = userInList.get(item.id); + listToggle.setVisibility(View.VISIBLE); + listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked); + listToggle.setOnClickListener(this::onClickToggle); + } else { + listToggle.setVisibility(View.GONE); + } + } + + private void onClickToggle(View view) { + saveListMembership(item.id, listToggle.isChecked()); + } + + @Override + public void onClick() { + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putString("listID", item.id); + args.putString("listTitle", item.title); + if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal()); + Nav.go(getActivity(), ListTimelineFragment.class, args); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java index 06719c24d..a80c9d192 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java @@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; import android.app.Fragment; +import android.app.assist.AssistContent; import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; @@ -13,6 +14,12 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; + +import com.squareup.otto.Subscribe; + import org.joinmastodon.android.E; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; @@ -24,12 +31,7 @@ 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 androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.widget.ViewPager2; - -import com.squareup.otto.Subscribe; +import org.joinmastodon.android.utils.ProvidesAssistContent; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.utils.V; -public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, DomainDisplay{ +public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent { private TabLayout tabLayout; private ViewPager2 pager; @@ -47,12 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment; private String accountID; - - @Override - public String getDomain() { - return DomainDisplay.super.getDomain() + "/notifications"; - } - @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); @@ -107,6 +103,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc tabLayout=view.findViewById(R.id.tabbar); pager=view.findViewById(R.id.pager); + UiUtils.reduceSwipeSensitivity(pager); tabViews=new FrameLayout[3]; for(int i=0;i() { @Override public void onSuccess(HeaderPaginationList accounts) { + if (getActivity() == null) return; getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty()); } @@ -228,6 +235,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc protected void updateToolbar(){ super.updateToolbar(); getToolbar().setOutlineProvider(null); + getToolbar().setOnClickListener(v->scrollToTop()); } private NotificationsListFragment getFragmentForPage(int page){ @@ -239,6 +247,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc }; } + @Override + public void onProvideAssistContent(AssistContent assistContent) { + callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent); + } + private class DiscoverPagerAdapter extends RecyclerView.Adapter{ @NonNull @Override @@ -263,4 +276,4 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc return position; } } -} \ No newline at end of file +} 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 c9c409b3f..176adcfe1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.View; @@ -10,6 +11,7 @@ import com.squareup.otto.Subscribe; import org.joinmastodon.android.E; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.markers.SaveMarkers; +import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.AllNotificationsSeenEvent; import org.joinmastodon.android.events.PollUpdatedEvent; @@ -18,6 +20,8 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.Instance; +import org.joinmastodon.android.model.Markers; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; @@ -53,11 +57,6 @@ public class NotificationsListFragment extends BaseStatusListFragment(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES)); + pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID))); } @Override 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 2bc81efa1..7e35672e1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -6,6 +6,7 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Activity; import android.app.Fragment; +import android.app.assist.AssistContent; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Color; @@ -67,6 +68,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.SimpleViewHolder; @@ -83,6 +85,7 @@ import org.joinmastodon.android.ui.views.CoverImageView; import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; import org.joinmastodon.android.ui.views.ProgressBarButton; +import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import java.time.LocalDateTime; @@ -93,9 +96,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.viewpager2.widget.ViewPager2; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -116,7 +123,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{ +public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri { private static final int AVATAR_RESULT=722; private static final int COVER_RESULT=343; @@ -146,6 +153,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private String note; private Account account; private String accountID; + private String domain; private Relationship relationship; private int statusBarHeight; private boolean isOwnProfile; @@ -160,7 +168,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private PhotoViewer currentPhotoViewer; private boolean editModeLoading; - private static final int MAX_FIELDS=4; + private int maxFields = 4; // from ProfileAboutFragment public UsableRecyclerView list; @@ -181,6 +189,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList setRetainInstance(true); accountID=getArguments().getString("account"); + domain=AccountSessionManager.getInstance().getAccount(accountID).domain; if(getArguments().containsKey("profileAccount")){ account=Parcels.unwrap(getArguments().getParcelable("profileAccount")); profileAccountID=account.id; @@ -188,6 +197,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList loaded=true; if(!isOwnProfile) loadRelationship(); + else if (isInstanceAkkoma() && getInstance().isPresent()) + maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields; }else{ profileAccountID=getArguments().getString("profileAccountID"); if(!getArguments().getBoolean("noAutoLoad", false)) @@ -206,14 +217,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList setHasOptionsMenu(true); } - @Override - public void onHiddenChanged(boolean hidden) { - super.onHiddenChanged(hidden); - if (!hidden) { - DomainManager.getInstance().setCurrentDomain(account.url); - } - } - @Override public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ View content=inflater.inflate(R.layout.fragment_profile, container, false); @@ -396,7 +399,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList username.setOnLongClickListener(v->{ String usernameString=account.acct; if(!usernameString.contains("@")){ - usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain; + usernameString+="@"+domain; } UiUtils.copyText(username, '@'+usernameString); return true; @@ -469,11 +472,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList @Override public void onRefresh(){ - if(isInEditMode){ - refreshing=false; - refreshLayout.setRefreshing(false); - return; - } if(refreshing) return; refreshing=true; @@ -577,12 +575,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private void bindHeaderView(){ setTitle(account.displayName); setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount)); - if((GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic) != null){ - ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100))); - } - if((GlobalUserPreferences.playGifs ? account.header : account.headerStatic) != null) { - ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000)); - } + 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); name.setText(ssb); @@ -610,7 +604,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList ssb.append(account.acct); if(isSelf){ ssb.append('@'); - ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain); + ssb.append(domain); } ssb.append(" "); Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate(); @@ -633,7 +627,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList username.setText(ssb); }else{ // noinspection SetTextI18n - username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : "")); + username.setText('@'+account.acct+(isSelf ? ('@'+domain) : "")); } CharSequence parsedBio = null; if(account.note != null){ @@ -645,8 +639,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList bio.setVisibility(View.VISIBLE); bio.setText(parsedBio); } - - followersCount.setText(UiUtils.abbreviateNumber(account.followersCount)); followingCount.setText(UiUtils.abbreviateNumber(account.followingCount)); postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount)); @@ -825,7 +817,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList args.putString("profileAccount", profileAccountID); args.putString("profileDisplayUsername", account.getDisplayUsername()); } - Nav.go(getActivity(), ListTimelinesFragment.class, args); + Nav.go(getActivity(), ListsFragment.class, args); }else if(id==R.id.followed_hashtags){ Bundle args=new Bundle(); args.putString("account", accountID); @@ -878,7 +870,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList // aboutFragment.setNote(relationship.note, accountID, profileAccountID); } notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username)); - DomainManager.getInstance().setCurrentDomain(account.url); } public ImageButton getFab() { @@ -1215,11 +1206,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList scrollView.smoothScrollTo(0, 0); } - @Override - public boolean isScrolledToTop() { - return list.getChildAt(0).getTop() == 0; - } - private void onFollowersOrFollowingClick(View v){ Bundle args=new Bundle(); args.putString("account", accountID); @@ -1284,6 +1270,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList if (adapter != null) adapter.notifyDataSetChanged(); } + @Override + public void onProvideAssistContent(AssistContent assistContent) { + callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent); + } + + @Override + public String getAccountID() { + return accountID; + } + + @Override + public Uri getWebUri(Uri.Builder base) { + return Uri.parse(account.url); + } + private class MetadataAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter { public MetadataAdapter(){ super(imgLoader); @@ -1314,7 +1315,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList public int getItemCount(){ if(isInEditMode){ int size=metadataListData.size(); - if(size items=new ArrayList<>(); + private ThemeItem themeItem; + private NotificationPolicyItem notificationPolicyItem; + private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem; + private ButtonItem defaultContentTypeButtonItem; + private String accountID; + private boolean needUpdateNotificationSettings; + private boolean needAppRestart; + private PushSubscription pushSubscription; + + private ImageView themeTransitionWindowView; + private TextItem checkForUpdateItem, clearImageCacheItem; + private ImageCache imageCache; + private Menu contentTypeMenu; + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) + setRetainInstance(true); + setTitle(R.string.settings); + imageCache = ImageCache.getInstance(getActivity()); + accountID=getArguments().getString("account"); + AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); + Optional instance = session.getInstance(); + String instanceName = UiUtils.getInstanceName(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 ButtonItem(R.string.sk_settings_color_palette, R.drawable.ic_fluent_color_24_regular, b->{ + PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL); + popupMenu.inflate(R.menu.color_palettes); + popupMenu.getMenu().findItem(R.id.m3_color).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S); + popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick); + b.setOnTouchListener(popupMenu.getDragToOpenListener()); + b.setOnClickListener(v->popupMenu.show()); + b.setText(switch(GlobalUserPreferences.color){ + case MATERIAL3 -> R.string.sk_color_palette_material3; + case PINK -> R.string.sk_color_palette_pink; + case PURPLE -> R.string.sk_color_palette_purple; + case GREEN -> R.string.sk_color_palette_green; + case BLUE -> R.string.sk_color_palette_blue; + case BROWN -> R.string.sk_color_palette_brown; + case RED -> R.string.sk_color_palette_red; + case YELLOW -> R.string.sk_color_palette_yellow; + }); + })); + items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b->{ + updatePublishText(b); + + b.setOnClickListener(l->{ + TextInputFrameLayout input = new TextInputFrameLayout( + getContext(), + getString(R.string.publish), + GlobalUserPreferences.publishButtonText.trim() + ); + new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(input) + .setPositiveButton(R.string.save, (d, which) -> { + GlobalUserPreferences.publishButtonText = input.getEditText().getText().toString().trim(); + GlobalUserPreferences.save(); + updatePublishText(b); + }) + .setNeutralButton(R.string.clear, (d, which) -> { + GlobalUserPreferences.publishButtonText = ""; + GlobalUserPreferences.save(); + updatePublishText(b); + }) + .setNegativeButton(R.string.cancel, (d, which) -> {}) + .show(); + }); + })); + items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{ + GlobalUserPreferences.uniformNotificationIcon=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{ + GlobalUserPreferences.disableMarquee=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{ + GlobalUserPreferences.reduceMotion=i.checked; + GlobalUserPreferences.save(); + })); + + 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 SwitchItem(R.string.sk_settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{ + GlobalUserPreferences.showInteractionCounts=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{ + GlobalUserPreferences.alwaysExpandContentWarnings=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{ + GlobalUserPreferences.disableSwipe=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{ + GlobalUserPreferences.enableDeleteNotifications=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(new SwitchItem(R.string.sk_settings_disable_alt_text_reminder, R.drawable.ic_fluent_image_alt_text_24_regular, GlobalUserPreferences.disableAltTextReminder, i->{ + GlobalUserPreferences.disableAltTextReminder=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_single_notification, R.drawable.ic_fluent_convert_range_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{ + GlobalUserPreferences.keepOnlyLatestNotification=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{ + GlobalUserPreferences.prefixRepliesWithRe=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{ + GlobalUserPreferences.confirmBeforeReblog=i.checked; + GlobalUserPreferences.save(); + })); + + items.add(new HeaderItem(R.string.sk_timelines)); + items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{ + GlobalUserPreferences.showReplies=i.checked; + GlobalUserPreferences.save(); + })); + if (isInstanceAkkoma()) { + items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{ + PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL); + popupMenu.inflate(R.menu.reply_visibility); + popupMenu.setOnMenuItemClickListener(item -> this.onReplyVisibilityChanged(item, b)); + b.setOnTouchListener(popupMenu.getDragToOpenListener()); + b.setOnClickListener(v->popupMenu.show()); + b.setText(GlobalUserPreferences.replyVisibility == null ? + R.string.sk_settings_reply_visibility_all : + switch(GlobalUserPreferences.replyVisibility){ + case "following" -> R.string.sk_settings_reply_visibility_following; + case "self" -> R.string.sk_settings_reply_visibility_self; + default -> R.string.sk_settings_reply_visibility_all; + }); + })); + } + items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{ + GlobalUserPreferences.showBoosts=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{ + GlobalUserPreferences.loadNewPosts=i.checked; + showNewPostsItem.enabled = i.checked; + if (!i.checked) { + GlobalUserPreferences.showNewPostsButton = false; + showNewPostsItem.checked = false; + } + if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsItem)) instanceof SwitchViewHolder svh) svh.rebind(); + GlobalUserPreferences.save(); + })); + items.add(showNewPostsItem = new SwitchItem(R.string.sk_settings_see_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{ + GlobalUserPreferences.showNewPostsButton=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_show_alt_indicator, R.drawable.ic_fluent_scan_text_24_regular, GlobalUserPreferences.showAltIndicator, i->{ + GlobalUserPreferences.showAltIndicator=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{ + GlobalUserPreferences.showNoAltIndicator=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_collapse_long_posts, R.drawable.ic_fluent_chevron_down_24_regular, GlobalUserPreferences.collapseLongPosts, i->{ + GlobalUserPreferences.collapseLongPosts=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{ + GlobalUserPreferences.spectatorMode=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(new SwitchItem(R.string.sk_settings_hide_fab, R.drawable.ic_fluent_edit_24_regular, GlobalUserPreferences.autoHideFab, i->{ + GlobalUserPreferences.autoHideFab=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(new SwitchItem(R.string.sk_reply_line_above_avatar, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.replyLineAboveHeader, i->{ + GlobalUserPreferences.replyLineAboveHeader=i.checked; + GlobalUserPreferences.compactReblogReplyLine=i.checked; + compactReblogReplyLineItem.enabled=i.checked; + compactReblogReplyLineItem.checked= GlobalUserPreferences.replyLineAboveHeader; + if (list.findViewHolderForAdapterPosition(items.indexOf(compactReblogReplyLineItem)) instanceof SwitchViewHolder svh) svh.rebind(); + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(compactReblogReplyLineItem=new SwitchItem(R.string.sk_compact_reblog_reply_line, R.drawable.ic_fluent_re_order_24_regular, GlobalUserPreferences.compactReblogReplyLine, i->{ + GlobalUserPreferences.compactReblogReplyLine=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + compactReblogReplyLineItem.enabled=GlobalUserPreferences.replyLineAboveHeader; + items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{ + GlobalUserPreferences.translateButtonOpenedOnly=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + boolean translationAvailable = instance + .map(i -> i.v2 != null && i.v2.configuration.translation != null && i.v2.configuration.translation.enabled) + .orElse(false); + items.add(new SmallTextItem(getString(translationAvailable ? + R.string.sk_settings_translation_availability_note_available : + R.string.sk_settings_translation_availability_note_unavailable, instanceName))); + + items.add(new HeaderItem(R.string.settings_notifications)); + items.add(notificationPolicyItem=new NotificationPolicyItem()); + PushSubscription pushSubscription=getPushSubscription(); + boolean switchEnabled=pushSubscription.policy!=PushSubscription.Policy.NONE; + + 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), switchEnabled)); + 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), switchEnabled)); + 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), switchEnabled)); + items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked), switchEnabled)); + items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_chat_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked), switchEnabled)); + items.add(new SwitchItem(R.string.sk_notify_update, R.drawable.ic_fluent_history_24_regular, pushSubscription.alerts.update, i->onNotificationsChanged(PushNotification.Type.UPDATE, i.checked), switchEnabled)); + items.add(new SwitchItem(R.string.sk_notify_poll_results, R.drawable.ic_fluent_poll_24_regular, pushSubscription.alerts.poll, i->onNotificationsChanged(PushNotification.Type.POLL, i.checked), switchEnabled)); + + items.add(new HeaderItem(R.string.settings_account)); + items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.sk_settings_filters, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/filters"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular)); + + items.add(new HeaderItem(instanceName)); + items.add(new TextItem(R.string.sk_settings_rules, instance.map(i -> () -> { + Bundle args = new Bundle(); + args.putParcelable("instance", Parcels.wrap(i)); + Nav.go(getActivity(), InstanceRulesFragment.class, args); + }).orElse(null), R.drawable.ic_fluent_task_list_ltr_24_regular)); + items.add(new TextItem(R.string.sk_settings_about_instance , ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/about"), R.drawable.ic_fluent_info_24_regular)); + items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular)); + items.add(new SmallTextItem(instance + .map(i -> getString(R.string.sk_settings_server_version, i.version)) + .orElse(getString(R.string.sk_instance_info_unavailable)))); + + items.add(new HeaderItem(R.string.sk_instance_features)); + items.add(new SwitchItem(R.string.sk_settings_content_types, 0, GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID), (i)->{ + if (i.checked) { + GlobalUserPreferences.accountsWithContentTypesEnabled.add(accountID); + if (GlobalUserPreferences.accountsDefaultContentTypes.get(accountID) == null) { + GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, ContentType.PLAIN); + } + } else { + GlobalUserPreferences.accountsWithContentTypesEnabled.remove(accountID); + GlobalUserPreferences.accountsDefaultContentTypes.remove(accountID); + } + if (list.findViewHolderForAdapterPosition(items.indexOf(defaultContentTypeButtonItem)) + instanceof ButtonViewHolder bvh) bvh.rebind(); + GlobalUserPreferences.save(); + })); + items.add(new SmallTextItem(getString(R.string.sk_settings_content_types_explanation))); + items.add(defaultContentTypeButtonItem = new ButtonItem(R.string.sk_settings_default_content_type, 0, b->{ + PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL); + popupMenu.inflate(R.menu.compose_content_type); + popupMenu.setOnMenuItemClickListener(item -> this.onContentTypeChanged(item, b)); + b.setOnTouchListener(popupMenu.getDragToOpenListener()); + b.setOnClickListener(v->popupMenu.show()); + ContentType contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID); + b.setText(getContentTypeString(contentType)); + contentTypeMenu = popupMenu.getMenu(); + contentTypeMenu.findItem(ContentType.getContentTypeRes(contentType)).setChecked(true); + instance.ifPresent(i -> ContentType.adaptMenuToInstance(contentTypeMenu, i)); + })); + items.add(new SmallTextItem(getString(R.string.sk_settings_default_content_type_explanation))); + items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{ + glitchModeItem.enabled = i.checked; + if (i.checked) { + GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID); + if (!isInstanceAkkoma()) { + GlobalUserPreferences.accountsInGlitchMode.add(accountID); + } + } else { + GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID); + GlobalUserPreferences.accountsInGlitchMode.remove(accountID); + } + glitchModeItem.checked = GlobalUserPreferences.accountsInGlitchMode.contains(accountID); + if (list.findViewHolderForAdapterPosition(items.indexOf(glitchModeItem)) instanceof SwitchViewHolder svh) svh.rebind(); + GlobalUserPreferences.save(); + })); + items.add(new SmallTextItem(getString(R.string.sk_settings_local_only_explanation))); + items.add(glitchModeItem = new SwitchItem(R.string.sk_settings_glitch_instance, 0, GlobalUserPreferences.accountsInGlitchMode.contains(accountID), i->{ + if (i.checked) { + GlobalUserPreferences.accountsInGlitchMode.add(accountID); + } else { + GlobalUserPreferences.accountsInGlitchMode.remove(accountID); + } + GlobalUserPreferences.save(); + })); + glitchModeItem.enabled = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID); + items.add(new SmallTextItem(getString(R.string.sk_settings_glitch_mode_explanation))); + + items.add(new HeaderItem(R.string.sk_settings_about)); + items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular)); + items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular)); + LruCache cache = imageCache == null ? null : imageCache.getLruCache(); + clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), cache != null ? cache.size() : 0, true), this::clearImageCache, 0); + items.add(clearImageCacheItem); + items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{ + GlobalUserPreferences.recentLanguages.remove(accountID); + GlobalUserPreferences.save(); + }))); + if (GithubSelfUpdater.needSelfUpdating()) { + items.add(new SwitchItem(R.string.sk_updater_enable_pre_releases, 0, GlobalUserPreferences.enablePreReleases, i->{ + GlobalUserPreferences.enablePreReleases=i.checked; + GlobalUserPreferences.save(); + })); + checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates); + items.add(checkForUpdateItem); + } + + 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.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE))); + } + + private void updatePublishText(Button btn) { + if (GlobalUserPreferences.publishButtonText.isBlank()) btn.setText(R.string.publish); + else btn.setText(GlobalUserPreferences.publishButtonText); + } + + @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); + } + if(needAppRestart) UiUtils.restartApp(); + } + + @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 boolean onColorPreferenceClick(MenuItem item){ + ColorPreference pref = null; + int id = item.getItemId(); + + if (id == R.id.m3_color) pref = ColorPreference.MATERIAL3; + else if (id == R.id.pink_color) pref = ColorPreference.PINK; + else if (id == R.id.purple_color) pref = ColorPreference.PURPLE; + else if (id == R.id.green_color) pref = ColorPreference.GREEN; + else if (id == R.id.blue_color) pref = ColorPreference.BLUE; + else if (id == R.id.brown_color) pref = ColorPreference.BROWN; + else if (id == R.id.red_color) pref = ColorPreference.RED; + else if (id == R.id.yellow_color) pref = ColorPreference.YELLOW; + + if (pref == null) return false; + + GlobalUserPreferences.color=pref; + GlobalUserPreferences.save(); + restartActivityToApplyNewTheme(); + return true; + } + + 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 @StringRes int getContentTypeString(@Nullable ContentType contentType) { + if (contentType == null) return R.string.sk_content_type_unspecified; + return switch (contentType) { + case PLAIN -> R.string.sk_content_type_plain; + case HTML -> R.string.sk_content_type_html; + case MARKDOWN -> R.string.sk_content_type_markdown; + case BBCODE -> R.string.sk_content_type_bbcode; + case MISSKEY_MARKDOWN -> R.string.sk_content_type_mfm; + }; + } + + private boolean onContentTypeChanged(MenuItem item, Button btn){ + int id = item.getItemId(); + ContentType contentType = switch (id) { + case R.id.content_type_plain -> ContentType.PLAIN; + case R.id.content_type_html -> ContentType.HTML; + case R.id.content_type_markdown -> ContentType.MARKDOWN; + case R.id.content_type_bbcode -> ContentType.BBCODE; + case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN; + default -> null; + }; + GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType); + GlobalUserPreferences.save(); + btn.setText(getContentTypeString(contentType)); + item.setChecked(true); + return true; + } + + private boolean onReplyVisibilityChanged(MenuItem item, Button btn){ + String pref = null; + int id = item.getItemId(); + + if (id == R.id.reply_visibility_following) pref = "following"; + else if (id == R.id.reply_visibility_self) pref = "self"; + + GlobalUserPreferences.replyVisibility=pref; + GlobalUserPreferences.save(); + btn.setText(GlobalUserPreferences.replyVisibility == null ? + R.string.sk_settings_reply_visibility_all : + switch(GlobalUserPreferences.replyVisibility){ + case "following" -> R.string.sk_settings_reply_visibility_following; + case "self" -> R.string.sk_settings_reply_visibility_self; + default -> R.string.sk_settings_reply_visibility_all; + }); + return true; + } + + 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=enabled; + case POLL -> subscription.alerts.poll=enabled; + case STATUS -> subscription.alerts.status=enabled; + case UPDATE -> subscription.alerts.update=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)){ + boolean newState=policy!=PushSubscription.Policy.NONE; + for(PushNotification.Type value : PushNotification.Type.values()){ + onNotificationsChanged(value, newState); + } + index++; + while(items.get(index) instanceof SwitchItem si){ + si.enabled=si.checked=newState; + 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(){ + if (getActivity() == null) return; + AccountSessionManager.getInstance().removeAccount(accountID); + getActivity().finish(); + Intent intent=new Intent(getActivity(), MainActivity.class); + startActivity(intent); + } + + private void clearImageCache(){ + MastodonAPIController.runInBackground(()->{ + Activity activity=getActivity(); + imageCache.clear(); + Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show(); + }); + if (list.findViewHolderForAdapterPosition(items.indexOf(clearImageCacheItem)) instanceof TextViewHolder tvh) { + clearImageCacheItem.secondaryText = UiUtils.formatFileSize(getContext(), 0, true); + tvh.rebind(); + } + } + + @Subscribe + public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){ + checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING; + if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind(); + + UpdateItem updateItem = null; + if(items.get(0) instanceof UpdateItem item0) { + updateItem = item0; + } else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING + && ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) { + updateItem = new UpdateItem(); + items.add(0, updateItem); + list.setAdapter(new SettingsAdapter()); + } + + if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){ + uvh.bind(updateItem); + } + + if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) { + Toast.makeText(getActivity(), R.string.sk_no_update_available, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public Uri getWebUri(Uri.Builder base) { + return base.path(isInstanceAkkoma() ? "/about" : "/settings").build(); + } + + @Override + public String getAccountID() { + return accountID; + } + + 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, @DrawableRes 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; + } + } + + public class ButtonItem extends Item{ + private int text; + private int icon; + private Consumer