diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 3145761a..8fc25ecc 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -13,7 +13,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 33 - versionCode 98 + versionCode 99 versionName "2.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -90,7 +90,7 @@ dependencies { implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0' implementation 'me.grishka.litex:palette:1.0.0' - implementation 'me.grishka.appkit:appkit:1.2.17' + implementation 'me.grishka.appkit:appkit:1.3.0' implementation 'com.google.code.gson:gson:2.8.9' implementation 'org.jsoup:jsoup:1.14.3' implementation 'com.squareup:otto:1.3.8' diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 1cc532fd..7a97cfce 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -31,9 +31,11 @@ android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config" android:icon="@mipmap/ic_launcher" android:theme="@style/Theme.Mastodon.AutoLightDark" - android:largeHeap="true"> + android:largeHeap="true" + android:enableOnBackInvokedCallback="true"> + diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 02cc15b4..0fc11997 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -61,6 +61,7 @@ public class MainActivity extends FragmentStackActivity{ @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); + setIntent(intent); if(intent.getBooleanExtra("fromNotification", false)){ String accountID=intent.getStringExtra("accountID"); AccountSession accountSession; @@ -85,6 +86,8 @@ public class MainActivity extends FragmentStackActivity{ showCompose(); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ handleURL(intent.getData(), null); + }else if(intent.getBooleanExtra("explore", false)){ + restartHomeFragment(); }/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){ GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this); }*/ @@ -211,6 +214,8 @@ public class MainActivity extends FragmentStackActivity{ } }else if(intent.getBooleanExtra("compose", false)){ showCompose(); + }else if(intent.getBooleanExtra("explore", false) && fragment instanceof HomeFragment hf){ + getWindow().getDecorView().post(()->hf.setCurrentTab(R.id.tab_search)); }else if(Intent.ACTION_VIEW.equals(intent.getAction())){ handleURL(intent.getData(), null); }else{ @@ -218,4 +223,10 @@ public class MainActivity extends FragmentStackActivity{ } } } + + public Fragment getTopmostFragment(){ + if(fragmentContainers.isEmpty()) + return null; + return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId()); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java new file mode 100644 index 00000000..6250d391 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/NotificationActionHandlerService.java @@ -0,0 +1,177 @@ +package org.joinmastodon.android; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.service.notification.StatusBarNotification; + +import org.joinmastodon.android.api.requests.statuses.CreateStatus; +import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited; +import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; +import org.joinmastodon.android.events.StatusCountersUpdatedEvent; +import org.joinmastodon.android.events.StatusCreatedEvent; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.StatusPrivacy; + +import java.util.UUID; + +import androidx.annotation.Nullable; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; + +public class NotificationActionHandlerService extends Service{ + private static final String TAG="NotificationActionHandl"; + private int runningRequestCount=0; + + @Nullable + @Override + public IBinder onBind(Intent intent){ + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId){ + String action=intent.getStringExtra("action"); + String account=intent.getStringExtra("account"); + String postID=intent.getStringExtra("post"); + String notificationTag=intent.getStringExtra("notificationTag"); + if(action==null || account==null || postID==null || notificationTag==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + NotificationManager nm=getSystemService(NotificationManager.class); + StatusBarNotification notification=findNotification(notificationTag); + if("reply".equals(action)){ + Bundle remoteInputResults=RemoteInput.getResultsFromIntent(intent); + if(remoteInputResults==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + CharSequence replyText=remoteInputResults.getCharSequence("replyText"); + if(replyText==null){ + maybeStopSelf(); + return START_NOT_STICKY; + } + CreateStatus.Request req=new CreateStatus.Request(); + req.inReplyToId=postID; + req.status=intent.getStringExtra("replyPrefix")+replyText; + req.visibility=StatusPrivacy.valueOf(intent.getStringExtra("visibility")); + runningRequestCount++; + new CreateStatus(req, UUID.randomUUID().toString()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCreatedEvent(result, account)); + if(notification!=null){ + Notification n=notification.getNotification(); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(NotificationActionHandlerService.this); + if(notification!=null){ + Notification n=notification.getNotification(); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + }else if("favorite".equals(action)){ + PendingIntent prevActionIntent; + if(notification!=null){ + Notification n=notification.getNotification(); + prevActionIntent=n.actions[1].actionIntent; + n.actions[1].actionIntent=null; + n.actions[1].title=getString(R.string.button_favorited); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + }else{ + prevActionIntent=null; + } + runningRequestCount++; + new SetStatusFavorited(postID, true) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCountersUpdatedEvent(result)); + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + if(notification!=null){ + Notification n=notification.getNotification(); + n.actions[1].actionIntent=prevActionIntent; + n.actions[1].title=getString(R.string.button_favorite); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + error.showToast(NotificationActionHandlerService.this); + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + }else if("boost".equals(action)){ + PendingIntent prevActionIntent; + if(notification!=null){ + Notification n=notification.getNotification(); + prevActionIntent=n.actions[2].actionIntent; + n.actions[2].actionIntent=null; + n.actions[2].title=getString(R.string.button_reblogged); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + }else{ + prevActionIntent=null; + } + runningRequestCount++; + new SetStatusReblogged(postID, true) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + E.post(new StatusCountersUpdatedEvent(result)); + runningRequestCount--; + maybeStopSelf(); + } + + @Override + public void onError(ErrorResponse error){ + if(notification!=null){ + Notification n=notification.getNotification(); + n.actions[2].actionIntent=prevActionIntent; + n.actions[2].title=getString(R.string.button_reblog); + nm.notify(notificationTag, PushNotificationReceiver.NOTIFICATION_ID, n); + } + error.showToast(NotificationActionHandlerService.this); + runningRequestCount--; + maybeStopSelf(); + } + }) + .exec(account); + } + return START_NOT_STICKY; + } + + private void maybeStopSelf(){ + if(runningRequestCount==0) + stopSelf(); + } + + private StatusBarNotification findNotification(String tag){ + for(StatusBarNotification sbn:getSystemService(NotificationManager.class).getActiveNotifications()){ + if(tag.equals(sbn.getTag())){ + return sbn; + } + } + return null; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java index af3ca352..d7d18c3d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java +++ b/mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java @@ -5,14 +5,15 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.RemoteInput; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.Log; @@ -21,10 +22,13 @@ import org.joinmastodon.android.api.requests.notifications.GetNotificationByID; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Mention; import org.joinmastodon.android.model.PushNotification; +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.stream.Collectors; @@ -144,19 +148,114 @@ public class PushNotificationReceiver extends BroadcastReceiver{ .setContentText(pn.body) .setStyle(new Notification.BigTextStyle().bigText(pn.body)) .setSmallIcon(R.drawable.ic_ntf_logo) - .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(context, (accountID+pn.notificationId).hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli()) .setShowWhen(true) .setCategory(Notification.CATEGORY_SOCIAL) .setAutoCancel(true) + .setOnlyAlertOnce(true) .setLights(context.getColor(R.color.primary_700), 500, 1000) - .setColor(context.getColor(R.color.primary_700)); + .setColor(context.getColor(R.color.primary_700)) + .setGroup(accountID); if(avatar!=null){ builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar)); } if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){ builder.setSubText(accountName); } - nm.notify(accountID, NOTIFICATION_ID, builder.build()); + String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id); + if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){ + ArrayList mentions=new ArrayList<>(); + String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id; + if(!notification.status.account.id.equals(ownID)) + mentions.add('@'+notification.status.account.acct); + for(Mention mention:notification.status.mentions){ + if(mention.id.equals(ownID)) + continue; + String m='@'+mention.acct; + if(!mentions.contains(m)) + mentions.add(m); + } + String replyPrefix=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" "; + + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + Intent replyIntent=new Intent(context, NotificationActionHandlerService.class); + replyIntent.putExtra("action", "reply"); + replyIntent.putExtra("account", accountID); + replyIntent.putExtra("post", notification.status.id); + replyIntent.putExtra("notificationTag", notificationTag); + replyIntent.putExtra("visibility", notification.status.visibility.toString()); + replyIntent.putExtra("replyPrefix", replyPrefix); + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_reply_24px), + context.getString(R.string.button_reply), PendingIntent.getService(context, (accountID+pn.notificationId+"reply").hashCode(), replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE)) + .addRemoteInput(new RemoteInput.Builder("replyText").setLabel(context.getString(R.string.button_reply)).build()) + .build()); + } + + Intent favIntent=new Intent(context, NotificationActionHandlerService.class); + favIntent.putExtra("action", "favorite"); + favIntent.putExtra("account", accountID); + favIntent.putExtra("post", notification.status.id); + favIntent.putExtra("notificationTag", notificationTag); + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_star_24px), + context.getString(R.string.button_favorite), PendingIntent.getService(context, (accountID+pn.notificationId+"favorite").hashCode(), favIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)).build()); + + PendingIntent boostActionIntent; + if(notification.status.visibility!=StatusPrivacy.DIRECT){ + Intent boostIntent=new Intent(context, NotificationActionHandlerService.class); + boostIntent.putExtra("action", "boost"); + boostIntent.putExtra("account", accountID); + boostIntent.putExtra("post", notification.status.id); + boostIntent.putExtra("notificationTag", notificationTag); + boostActionIntent=PendingIntent.getService(context, (accountID+pn.notificationId+"boost").hashCode(), boostIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + }else{ + boostActionIntent=null; + } + builder.addAction(new Notification.Action.Builder(Icon.createWithResource(context, R.drawable.ic_boost_24px), + context.getString(R.string.button_reblog), boostActionIntent).build()); + } + nm.notify(notificationTag, NOTIFICATION_ID, builder.build()); + + StatusBarNotification[] activeNotifications=nm.getActiveNotifications(); + ArrayList summaryLines=new ArrayList<>(); + int notificationCount=0; + for(StatusBarNotification sbn:activeNotifications){ + String tag=sbn.getTag(); + if(tag!=null && tag.startsWith(accountID+"_")){ + if((sbn.getNotification().flags & Notification.FLAG_GROUP_SUMMARY)==0){ + if(summaryLines.size()<5){ + summaryLines.add(sbn.getNotification().extras.getString("android.title")); + } + notificationCount++; + } + } + } + + if(summaryLines.size()>1){ + Notification.Builder summaryBuilder; + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ + summaryBuilder=new Notification.Builder(context, accountID+"_"+pn.notificationType); + }else{ + summaryBuilder=new Notification.Builder(context) + .setPriority(Notification.PRIORITY_DEFAULT); + } + Notification.InboxStyle inboxStyle=new Notification.InboxStyle(); + for(String line:summaryLines){ + inboxStyle.addLine(line); + } + summaryBuilder.setContentTitle(context.getString(R.string.app_name)) + .setContentText(context.getResources().getQuantityString(R.plurals.x_new_notifications, notificationCount, notificationCount)) + .setSmallIcon(R.drawable.ic_ntf_logo) + .setColor(context.getColor(R.color.primary_700)) + .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)) + .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli()) + .setShowWhen(true) + .setCategory(Notification.CATEGORY_SOCIAL) + .setAutoCancel(true) + .setGroup(accountID) + .setGroupSummary(true) + .setStyle(inboxStyle.setSummaryText(accountName)); + nm.notify(accountID+"_summary", NOTIFICATION_ID, summaryBuilder.build()); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java index 54a55df5..a1f6a159 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/catalog/GetCatalogInstances.java @@ -13,11 +13,13 @@ import java.util.List; public class GetCatalogInstances extends MastodonAPIRequest>{ private String lang, category; + private boolean includeClosedSignups; - public GetCatalogInstances(String lang, String category){ + public GetCatalogInstances(String lang, String category, boolean includeClosedSignups){ super(HttpMethod.GET, null, new TypeToken<>(){}); this.lang=lang; this.category=category; + this.includeClosedSignups=includeClosedSignups; } @Override @@ -30,6 +32,8 @@ public class GetCatalogInstances extends MastodonAPIRequest exten protected HashMap relationships=new HashMap<>(); protected Rect tmpRect=new Rect(); protected TypedObjectPool attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView); + private SpringAnimation listShakeAnimation; public BaseStatusListFragment(){ super(20); @@ -675,6 +679,17 @@ public abstract class BaseStatusListFragment exten protected void onModifyItemViewHolder(BindableViewHolder holder){} + public void shakeListView(){ + if(listShakeAnimation!=null) + listShakeAnimation.cancel(); + SpringAnimation anim=new SpringAnimation(list, DynamicAnimation.TRANSLATION_X, 0); + anim.setStartVelocity(V.dp(-500)); + anim.getSpring().setStiffness(500).setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY); + listShakeAnimation=anim; + anim.addEndListener((animation, canceled, value, velocity)->listShakeAnimation=null); + anim.start(); + } + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ 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 519d3cc7..c8d5960c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -102,13 +102,12 @@ import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.CustomTransitionsFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; -public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{ +public class ComposeFragment extends MastodonToolbarFragment implements ComposeEditText.SelectionListener, CustomTransitionsFragment{ private static final int MEDIA_RESULT=717; public static final int IMAGE_DESCRIPTION_RESULT=363; @@ -173,6 +172,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private BackgroundColorSpan overLimitBG; private ForegroundColorSpan overLimitFG; + private Runnable emojiKeyboardHider; + private Runnable sendingBackButtonBlocker; + private Runnable discardConfirmationCallback=this::confirmDiscardDraftAndFinish; + private boolean prevHadDraft; + public ComposeFragment(){ super(R.layout.toolbar_fragment_with_progressbar); } @@ -249,6 +253,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); } }); + emojiKeyboardHider=emojiKeyboard::hide; View view=inflater.inflate(R.layout.fragment_compose, container, false); mainLayout=view.findViewById(R.id.compose_main_ll); @@ -305,6 +310,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr public void onIconChanged(int icon){ emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN); updateNavigationBarColor(icon!=PopupKeyboard.ICON_HIDDEN); + if(icon!=PopupKeyboard.ICON_HIDDEN) + addBackCallback(emojiKeyboardHider); + else + removeBackCallback(emojiKeyboardHider); if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){ contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom()); if(icon==PopupKeyboard.ICON_HIDDEN) @@ -480,6 +489,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } updateCharCounter(); + updateDraftState(); } }); spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter())); @@ -621,6 +631,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(publishButton==null) return; publishButton.setEnabled((trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1)); + updateDraftState(); } private void onCustomEmojiClick(Emoji emoji){ @@ -696,6 +707,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; overlayParams.token=mainEditText.getWindowToken(); wm.addView(sendingOverlay, overlayParams); + addBackCallback(sendingBackButtonBlocker); publishButton.setEnabled(false); V.setVisibilityAnimated(sendProgress, View.VISIBLE); @@ -720,8 +732,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(!pollViewController.isEmpty()){ req.poll=pollViewController.getPollForRequest(); } - if(hasSpoiler && spoilerEdit.length()>0){ - req.spoilerText=spoilerEdit.getText().toString(); + if(hasSpoiler){ + if(spoilerEdit.length()>0) + req.spoilerText=spoilerEdit.getText().toString(); + else + req.sensitive=true; } if(postLang!=null){ req.language=postLang.locale.toLanguageTag(); @@ -734,6 +749,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr public void onSuccess(Status result){ wm.removeView(sendingOverlay); sendingOverlay=null; + removeBackCallback(sendingBackButtonBlocker); if(editingStatus==null){ E.post(new StatusCreatedEvent(result, accountID)); if(replyTo!=null){ @@ -766,6 +782,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void handlePublishError(ErrorResponse error){ wm.removeView(sendingOverlay); sendingOverlay=null; + removeBackCallback(sendingBackButtonBlocker); V.setVisibilityAnimated(sendProgress, View.GONE); publishButton.setEnabled(true); if(error instanceof MastodonErrorResponse me){ @@ -793,19 +810,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !mediaViewController.isEmpty() || pollFieldsHaveContent; } - @Override - public boolean onBackPressed(){ - if(emojiKeyboard.isVisible()){ - emojiKeyboard.hide(); - return true; + private void updateDraftState(){ + boolean hasDraft=hasDraft(); + if(hasDraft!=prevHadDraft){ + prevHadDraft=hasDraft; + if(hasDraft){ + addBackCallback(discardConfirmationCallback); + }else{ + removeBackCallback(discardConfirmationCallback); + } } - if(hasDraft()){ - confirmDiscardDraftAndFinish(); - return true; - } - if(sendingOverlay!=null) - return true; - return false; } @Override @@ -839,7 +853,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void confirmDiscardDraftAndFinish(){ new M3AlertDialogBuilder(getActivity()) .setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes) - .setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this)) + .setPositiveButton(R.string.discard, (dialog, which)->{ + removeBackCallback(discardConfirmationCallback); + Nav.finish(this); + }) .setNegativeButton(R.string.cancel, null) .show(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java index 5e3cc22c..efb6aa31 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java @@ -32,12 +32,11 @@ import java.util.Collections; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; -public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{ +public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ private static final String TAG="ComposeImageDescription"; private String accountID, attachmentID; @@ -138,9 +137,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp } @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); deliverResult(); - return false; } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java index afd92a0c..a5406726 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateListAddMembersFragment.java @@ -42,13 +42,11 @@ 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.fragments.OnBackPressedListener; -import me.grishka.appkit.fragments.WindowInsetsAwareFragment; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{ +public class CreateListAddMembersFragment extends BaseAccountListFragment implements AddNewListMembersFragment.Listener{ private FollowList followList; private Button nextButton; private View buttonBar; @@ -59,6 +57,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem private WindowInsets lastInsets; private boolean dismissingSearchFragment; private HashSet accountIDsInList=new HashSet<>(); + private Runnable searchFragmentDismisser=this::dismissSearchFragment; @Override public void onCreate(Bundle savedInstanceState){ @@ -156,6 +155,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ rootView.setVisibility(View.GONE); }).start(); + addBackCallback(searchFragmentDismisser); return true; } @@ -183,6 +183,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem private void dismissSearchFragment(){ if(searchFragment==null || dismissingSearchFragment) return; + removeBackCallback(searchFragmentDismisser); dismissingSearchFragment=true; rootView.setVisibility(View.VISIBLE); searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ @@ -201,15 +202,6 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem Nav.finish(this); } - @Override - public boolean onBackPressed(){ - if(searchFragment!=null){ - dismissSearchFragment(); - return true; - } - return false; - } - @Override public boolean isAccountInList(AccountViewModel account){ return accountIDsInList.contains(account.account.id); 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 8fdbb49a..dc7a76c5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -275,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{ }) .exec(accountID); } + + public String getHashtagName(){ + return hashtagName; + } } 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 f40283f0..7329a484 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -30,8 +30,8 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; -import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.utils.ObjectIdComparator; @@ -48,13 +48,12 @@ import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.LoaderFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; 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{ private FragmentRootLinearLayout content; private HomeTimelineFragment homeTimelineFragment; private NotificationsListFragment notificationsFragment; @@ -272,15 +271,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene return false; } - @Override - public boolean onBackPressed(){ - if(currentTab==R.id.tab_profile) - return profileFragment.onBackPressed(); - if(currentTab==R.id.tab_search) - return searchFragment.onBackPressed(); - return false; - } - @Override public void onSaveInstanceState(Bundle outState){ super.onSaveInstanceState(outState); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java index f6a5b364..13f5aba9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListMembersFragment.java @@ -44,12 +44,11 @@ import java.util.stream.Collectors; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{ +public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener{ private ImageButton fab; private FollowList followList; private boolean inSelectionMode; @@ -63,6 +62,8 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements private WindowInsets lastInsets; private HashSet accountIDsInList=new HashSet<>(); private boolean dismissingSearchFragment; + private Runnable searchFragmentDismisser=this::dismissSearchFragment;; + private Runnable actionModeDismisser=()->actionMode.finish(); public ListMembersFragment(){ setListLayoutId(R.layout.recycler_fragment_with_fab); @@ -214,6 +215,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{ rootView.setVisibility(View.GONE); }).start(); + addBackCallback(searchFragmentDismisser); } private void onItemClick(AccountViewHolder holder){ @@ -293,9 +295,11 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements selectedAccounts.clear(); updateItemsForSelectionModeTransition(); V.setVisibilityAnimated(fab, View.VISIBLE); + removeBackCallback(actionModeDismisser); } }); updateActionModeTitle(); + addBackCallback(actionModeDismisser); } private void updateActionModeTitle(){ @@ -371,15 +375,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements removeAccounts(Set.of(account.account.id), onDone); } - @Override - public boolean onBackPressed(){ - if(searchFragment!=null){ - dismissSearchFragment(); - return true; - } - return false; - } - private void dismissSearchFragment(){ if(searchFragment==null || dismissingSearchFragment) return; @@ -393,6 +388,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements searchFragment=null; dismissingSearchFragment=false; }).start(); + removeBackCallback(searchFragmentDismisser); getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); } 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 9602a1a9..0045291f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -100,14 +100,13 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.LoaderFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{ +public class ProfileFragment extends LoaderFragment implements ScrollableToTop{ private static final int AVATAR_RESULT=722; private static final int COVER_RESULT=343; @@ -158,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private Animator tabBarColorAnim; private MenuItem editSaveMenuItem; private boolean savingEdits; + private Runnable editModeBackCallback=this::onEditModeBackCallback; @Override public void onCreate(Bundle savedInstanceState){ @@ -687,7 +687,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername())); } - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){ + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()){ menu.setGroupDividerEnabled(true); } } @@ -983,12 +983,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList refreshLayout.setEnabled(false); editDirty=false; V.setVisibilityAnimated(fab, View.GONE); + addBackCallback(editModeBackCallback); } private void exitEditMode(){ if(!isInEditMode) throw new IllegalStateException(); isInEditMode=false; + removeBackCallback(editModeBackCallback); invalidateOptionsMenu(); actionButton.setText(R.string.edit_profile); @@ -1098,23 +1100,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList updateRelationship(); } - @Override - public boolean onBackPressed(){ - if(isInEditMode){ - if(savingEdits) - return true; - if(editDirty || aboutFragment.isEditDirty()){ - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.discard_changes) - .setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode()) - .setNegativeButton(R.string.cancel, null) - .show(); - }else{ - exitEditMode(); - } - return true; + private void onEditModeBackCallback(){ + if(savingEdits) + return; + if(editDirty || aboutFragment.isEditDirty()){ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.discard_changes) + .setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode()) + .setNegativeButton(R.string.cancel, null) + .show(); + }else{ + exitEditMode(); } - return false; } private List createFakeAttachments(String url, Drawable drawable){ @@ -1149,7 +1146,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList startImagePicker(COVER_RESULT); }else{ Drawable drawable=cover.getDrawable(); - if(drawable==null || drawable instanceof ColorDrawable) + if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png")) return; currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0, null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0))); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java index 919b13a7..32094a64 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java @@ -53,6 +53,8 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; @@ -144,12 +146,16 @@ public class ProfileQrCodeFragment extends AppKitFragment{ if(!isTablet){ getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } - dlg.setOnKeyListener((dialog, keyCode, event)->{ - if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){ - dismiss(); - } - return true; - }); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + dlg.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::dismiss); + }else{ + dlg.setOnKeyListener((dialog, keyCode, event)->{ + if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){ + dismiss(); + } + return true; + }); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java index a1934c14..ca93bdd4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java @@ -36,10 +36,9 @@ import androidx.viewpager2.widget.ViewPager2; import me.grishka.appkit.Nav; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.V; -public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{ +public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{ private static final int QUERY_RESULT=937; private static final int SCAN_RESULT=456; @@ -62,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, private String accountID; private String currentQuery; private Intent scannerIntent; + private Runnable searchExitCallback=this::exitSearch; @Override public void onCreate(Bundle savedInstanceState){ @@ -232,6 +232,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, searchBack.setEnabled(true); searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); tabsDivider.setVisibility(View.GONE); + addBackCallback(searchExitCallback); } } @@ -248,6 +249,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); tabsDivider.setVisibility(View.VISIBLE); currentQuery=null; + removeBackCallback(searchExitCallback); } private Fragment getFragmentForPage(int page){ @@ -260,15 +262,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, }; } - @Override - public boolean onBackPressed(){ - if(searchActive){ - exitSearch(); - return true; - } - return false; - } - @Override public void onFragmentResult(int reqCode, boolean success, Bundle result){ if(reqCode==QUERY_RESULT && success){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java index 3ff7fc79..16768a38 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java @@ -57,14 +57,13 @@ import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.CustomTransitionsFragment; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class SearchQueryFragment extends MastodonRecyclerFragment implements CustomTransitionsFragment, OnBackPressedListener{ +public class SearchQueryFragment extends MastodonRecyclerFragment implements CustomTransitionsFragment{ private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE); private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE); @@ -371,6 +370,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment onError){ - if(TextUtils.isEmpty(_domain)) + if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1) return; String domain=normalizeInstanceDomain(_domain); Instance cachedInstance=instancesCache.get(domain); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java index 56afdd3c..68a9c890 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogSignupFragment.java @@ -5,8 +5,9 @@ import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; import android.text.Editable; @@ -26,16 +27,12 @@ import android.widget.PopupMenu; import android.widget.RadioButton; import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.requests.accounts.CheckInviteLink; -import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.catalog.CatalogCategory; import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.M3AlertDialogBuilder; @@ -49,11 +46,9 @@ import org.parceler.Parcels; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.Random; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -63,17 +58,11 @@ import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.BindableViewHolder; -import me.grishka.appkit.utils.MergeRecyclerAdapter; -import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; -public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{ - private MastodonAPIRequest getCategoriesRequest; - private String currentCategory="all"; - private List categories=new ArrayList<>(); +public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{ private View topBar; private List languages=Collections.emptyList(); @@ -94,6 +83,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple private String inviteCode, inviteCodeHost; private AlertDialog currentInviteLinkAlert; + private Runnable exitQueryModeCallback=()->setSearchQueryMode(false); + public InstanceCatalogSignupFragment(){ super(R.layout.fragment_onboarding_common, 10); } @@ -113,7 +104,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple @Override protected void doLoadData(int offset, int count){ - currentRequest=new GetCatalogInstances(null, null) + currentRequest=new GetCatalogInstances(null, null, false) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ @@ -149,58 +140,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } }) .execNoAuth(""); - getCategoriesRequest=new GetCatalogCategories(null) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List result){ - getCategoriesRequest=null; - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add); - updateCategories(); - } - - @Override - public void onError(ErrorResponse error){ - getCategoriesRequest=null; - error.showToast(getActivity()); - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - updateCategories(); - } - }) - .execNoAuth(""); - } - - private void updateCategories(){ -// categoriesList.removeAllTabs(); -// for(CatalogCategory cat:categories){ -// int titleRes=getTitleForCategory(cat.category); -// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category); -// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji); -// emoji.setImageResource(getEmojiForCategory(cat.category)); -// categoriesList.addTab(tab); -// } } @Override public void onDestroy(){ super.onDestroy(); - if(getCategoriesRequest!=null) - getCategoriesRequest.cancel(); } @Override protected RecyclerView.Adapter getAdapter(){ - View headerView=new View(getActivity()); - headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - - mergeAdapter=new MergeRecyclerAdapter(); - mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); - mergeAdapter.addAdapter(adapter=new InstancesAdapter()); - return mergeAdapter; + return adapter=new InstancesAdapter(); } @SuppressLint("ClickableViewAccessibility") @@ -222,7 +171,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple setStatusBarColor(0); topBar=view.findViewById(R.id.top_bar); - list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar)); + list.addOnScrollListener(new ElevationOnScrollListener(null, topBar)); + if(buttonBar.getBackground() instanceof LayerDrawable ld){ + ld=(LayerDrawable) ld.mutate(); + buttonBar.setBackground(ld); + Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay); + if(overlay!=null){ + overlay.setAlpha(20); + } + } + buttonBar.setElevation(V.dp(3)); searchEdit=view.findViewById(R.id.search_edit); searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); @@ -572,6 +530,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple filteredData.add(instance); } } + setEmptyText(getString(R.string.no_servers_found, currentSearchQuery)); + }else{ + setEmptyText(""); } }else{ for(CatalogInstance instance:data){ @@ -591,27 +552,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple } } } - DiffUtil.calculateDiff(new DiffUtil.Callback(){ - @Override - public int getOldListSize(){ - return prevData.size(); - } + UiUtils.updateRecyclerViewKeepingAbsoluteScrollPosition(list, ()->{ + DiffUtil.calculateDiff(new DiffUtil.Callback(){ + @Override + public int getOldListSize(){ + return prevData.size(); + } - @Override - public int getNewListSize(){ - return filteredData.size(); - } + @Override + public int getNewListSize(){ + return filteredData.size(); + } - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } - }).dispatchUpdatesTo(adapter); + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } + }).dispatchUpdatesTo(adapter); + }); } @Override @@ -620,19 +583,13 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); } - @Override - public boolean onBackPressed(){ - if(searchQueryMode){ - setSearchQueryMode(false); - return true; - } - return false; - } - private void setSearchQueryMode(boolean enabled){ + if(searchQueryMode==enabled) + return; searchQueryMode=enabled; RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams(); if(searchQueryMode){ + addBackCallback(exitQueryModeCallback); filtersScroll.setVisibility(View.GONE); lp.removeRule(RelativeLayout.END_OF); backBtn.setScaleX(0.83333333f); @@ -640,6 +597,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple backBtn.setTranslationX(V.dp(8)); searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0)); }else{ + removeBackCallback(exitQueryModeCallback); filtersScroll.setVisibility(View.VISIBLE); focusThing.requestFocus(); searchEdit.setText(""); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java index e4ce6401..38ed1c53 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceChooserLoginFragment.java @@ -1,12 +1,11 @@ package org.joinmastodon.android.fragments.onboarding; -import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Rect; -import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.view.ViewGroup; @@ -65,7 +64,7 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ protected void updateFilteredList(){ ArrayList prevData=new ArrayList<>(filteredData); filteredData.clear(); - if(currentSearchQuery.length()>0){ + if(!TextUtils.isEmpty(currentSearchQuery)){ boolean foundExactMatch=false; for(CatalogInstance inst:data){ if(inst.normalizedDomain.contains(currentSearchQuery)){ @@ -74,9 +73,16 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ foundExactMatch=true; } } - if(!foundExactMatch) + if(!foundExactMatch && currentSearchQuery.indexOf('.')!=-1) filteredData.add(0, fakeInstance); } + if(filteredData.isEmpty()){ + for(CatalogInstance inst:data){ + if(inst.normalizedDomain.equals("mastodon.social") || inst.normalizedDomain.equals("mastodon.online")){ + filteredData.add(inst); + } + } + } UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals); for(int i=0;i(){ @Override public void onSuccess(List result){ data.clear(); data.addAll(sortInstances(result)); + updateFilteredList(); } @Override @@ -112,6 +119,9 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{ Toolbar toolbar=getToolbar(); toolbar.setElevation(0); toolbar.setBackground(null); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + toolbar.setContentInsetStartWithNavigation(V.dp(80)); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java index 26d30ab6..ea008cbf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/SignupFragment.java @@ -50,6 +50,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import androidx.annotation.Nullable; @@ -62,6 +63,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout; public class SignupFragment extends ToolbarFragment{ private static final String TAG="SignupFragment"; + private final Pattern emailRegex=Pattern.compile("^[^@]+@[^@]+\\.[^@]{2,}$"); private Instance instance; @@ -97,6 +99,7 @@ public class SignupFragment extends ToolbarFragment{ View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false); TextView domain=view.findViewById(R.id.domain); + TextView atSign=view.findViewById(R.id.at_sign); displayName=view.findViewById(R.id.display_name); username=view.findViewById(R.id.username); email=view.findViewById(R.id.email); @@ -118,7 +121,7 @@ public class SignupFragment extends ToolbarFragment{ @Override public boolean onPreDraw(){ username.getViewTreeObserver().removeOnPreDrawListener(this); - username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); + username.setPadding(atSign.getWidth(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom()); return true; } }); @@ -145,6 +148,10 @@ public class SignupFragment extends ToolbarFragment{ reasonExplain.setVisibility(View.GONE); } + password.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + passwordConfirm.setOnFocusChangeListener(this::onPasswordFieldFocusChange); + email.setOnFocusChangeListener(this::onEmailFieldFocusChange); + return view; } @@ -281,34 +288,44 @@ public class SignupFragment extends ToolbarFragment{ .exec(instance.uri, apiToken); } + private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){ + SpannableStringBuilder ssb=new SpannableStringBuilder(); + Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ + private int spanStart; + @Override + public void head(Node node, int depth){ + if(node instanceof TextNode tn){ + ssb.append(tn.text()); + }else if(node instanceof Element){ + spanStart=ssb.length(); + } + } + + @Override + public void tail(Node node, int depth){ + if(node instanceof Element){ + ssb.setSpan(new LinkSpan("", onClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + }); + return ssb; + } + private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){ return switch(fieldName){ case "email" -> switch(error.error){ case "ERR_BLOCKED" -> { String emailAddr=email.getText().toString(); String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1))); - SpannableStringBuilder ssb=new SpannableStringBuilder(); - Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){ - private int spanStart; - @Override - public void head(Node node, int depth){ - if(node instanceof TextNode tn){ - ssb.append(tn.text()); - }else if(node instanceof Element){ - spanStart=ssb.length(); - } - } - - @Override - public void tail(Node node, int depth){ - if(node instanceof Element){ - ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - }); - yield ssb; + yield makeLinkInErrorMessage(s, this::onGoBackLinkClick); } + case "ERR_INVALID" -> getString(R.string.signup_email_invalid); + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_email_taken), this::onForgotPasswordLinkClick); + default -> error.description; + }; + case "username" -> switch(error.error){ + case "ERR_TAKEN" -> makeLinkInErrorMessage(getString(R.string.signup_username_taken), this::onGoBackLinkClick); default -> error.description; }; default -> error.description; @@ -345,7 +362,9 @@ public class SignupFragment extends ToolbarFragment{ } private void updateButtonState(){ - btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0)); + btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find() + && password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString()) + && (!instance.approvalRequired || reason.length()>0)); } private void createAppAndGetToken(){ @@ -406,6 +425,24 @@ public class SignupFragment extends ToolbarFragment{ Nav.finish(this); } + private void onForgotPasswordLinkClick(LinkSpan span){ + UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new"); + } + + private void onPasswordFieldFocusChange(View v, boolean hasFocus){ + if(hasFocus || password.length()==0 || passwordConfirm.length()==0) + return; + if(!password.getText().toString().equals(passwordConfirm.getText().toString())){ + passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match)); + } + } + + private void onEmailFieldFocusChange(View v, boolean hasFocus){ + if(!hasFocus && email.length()>0 && !emailRegex.matcher(email.getText()).find()){ + emailWrap.setErrorState(getString(R.string.signup_email_invalid)); + } + } + private class ErrorClearingListener implements TextWatcher{ public final EditText editText; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java index 49a96320..83f1f43f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java @@ -24,6 +24,7 @@ import org.joinmastodon.android.model.FilterKeyword; import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout; import org.parceler.Parcels; @@ -44,11 +45,10 @@ import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter; -public class EditFilterFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class EditFilterFragment extends BaseSettingsFragment{ private static final int WORDS_RESULT=370; private static final int CONTEXT_RESULT=651; @@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment implements On private ArrayList deletedWordIDs=new ArrayList<>(); private EnumSet context=EnumSet.allOf(FilterContext.class); private boolean dirty; + private boolean wasDirty; + + private Runnable confirmCallback=()->{ + if(isDirty()){ + UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this)); + } + }; @Override public void onCreate(Bundle savedInstanceState){ @@ -101,6 +108,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On titleEditLayout.updateHint(); if(filter!=null) titleEdit.setText(filter.title); + titleEdit.addTextChangedListener(new SimpleTextWatcher(e->updateBackCallback())); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout)); @@ -158,6 +166,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } a.dismiss(); } + updateBackCallback(); }) .show(); alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); @@ -309,6 +318,7 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } deletedWordIDs.addAll(result.getStringArrayList("deleted")); } + updateBackCallback(); } } @@ -317,11 +327,19 @@ public class EditFilterFragment extends BaseSettingsFragment implements On } @Override - public boolean onBackPressed(){ - if(isDirty()){ - UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this)); - return true; + protected void toggleCheckableItem(ListItem item){ + super.toggleCheckableItem(item); + updateBackCallback(); + } + + private void updateBackCallback(){ + boolean dirty=isDirty(); + if(dirty!=wasDirty){ + wasDirty=dirty; + if(dirty) + addBackCallback(confirmCallback); + else + removeBackCallback(confirmCallback); } - return false; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java index 37d91107..a9850352 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterContextFragment.java @@ -11,9 +11,7 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.stream.Collectors; -import me.grishka.appkit.fragments.OnBackPressedListener; - -public class FilterContextFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class FilterContextFragment extends BaseSettingsFragment{ private EnumSet context; @Override @@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment i protected void doLoadData(int offset, int count){} @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); context=EnumSet.noneOf(FilterContext.class); for(ListItem item:data){ if(((CheckableListItem) item).checked) @@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment i Bundle args=new Bundle(); args.putSerializable("context", context); setResult(true, args); - return false; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java index 0dec195b..943050a9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/FilterWordsFragment.java @@ -1,7 +1,6 @@ package org.joinmastodon.android.fragments.settings; import android.app.AlertDialog; -import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.text.InputType; @@ -11,11 +10,9 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.Button; import android.widget.EditText; -import android.widget.ImageButton; import org.joinmastodon.android.R; import org.joinmastodon.android.model.FilterKeyword; @@ -33,15 +30,15 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.utils.V; -public class FilterWordsFragment extends BaseSettingsFragment implements OnBackPressedListener{ +public class FilterWordsFragment extends BaseSettingsFragment{ private Button fab; private ActionMode actionMode; private ArrayList> selectedItems=new ArrayList<>(); private ArrayList deletedItemIDs=new ArrayList<>(); private MenuItem deleteItem; + private Runnable actionModeDismisser=()->actionMode.finish(); public FilterWordsFragment(){ setListLayoutId(R.layout.recycler_fragment_with_text_fab); @@ -80,12 +77,12 @@ public class FilterWordsFragment extends BaseSettingsFragment imp } @Override - public boolean onBackPressed(){ + public void onStop(){ + super.onStop(); Bundle result=new Bundle(); result.putParcelableArrayList("words", (ArrayList) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new))); result.putStringArrayList("deleted", deletedItemIDs); setResult(true, result); - return false; } @Override @@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment imp } itemsAdapter.notifyItemRangeChanged(0, data.size()); updateActionModeTitle(); + addBackCallback(actionModeDismisser); } private void leaveSelectionMode(boolean fromActionMode){ @@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment imp data.set(i, newItem); } itemsAdapter.notifyItemRangeChanged(0, data.size()); + removeBackCallback(actionModeDismisser); } private void updateActionModeTitle(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java b/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java index d6f8a7db..0d916cae 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/catalog/CatalogInstance.java @@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName; import org.joinmastodon.android.api.AllFieldsAreRequired; import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.model.BaseModel; import java.net.IDN; @@ -15,14 +16,18 @@ import java.util.List; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; -@AllFieldsAreRequired public class CatalogInstance extends BaseModel{ + @RequiredField public String domain; + @RequiredField public String version; + @RequiredField public String description; + @RequiredField public List languages; @SerializedName("region") private String _region; + @RequiredField public List categories; public String proxiedThumbnail; public int totalUsers; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index c998434e..b5034f57 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -140,7 +140,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ optionsMenu=new PopupMenu(activity, more); optionsMenu.inflate(R.menu.post); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()) + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) optionsMenu.getMenu().setGroupDividerEnabled(true); optionsMenu.setOnMenuItemClickListener(menuItem->{ Account account=item.user; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java index 4d22f5cb..19cec6b7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java @@ -54,6 +54,7 @@ import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import android.widget.Toolbar; +import android.window.OnBackInvokedDispatcher; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; @@ -169,7 +170,7 @@ public class PhotoViewer implements ZoomPanView.Listener{ windowView=new FrameLayout(activity){ @Override public boolean dispatchKeyEvent(KeyEvent event){ - if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){ + if(Build.VERSION.SDK_INT=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); wm.addView(windowView, wlp); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + // TODO make use of the progress callback for nicer animation + windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->onStartSwipeToDismissTransition(0)); + } windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index a9c4cb57..c9a7e110 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -52,6 +52,7 @@ import android.widget.Toast; import org.joinmastodon.android.E; import org.joinmastodon.android.FileProvider; import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; @@ -368,6 +369,8 @@ public class UiUtils{ } public static void openHashtagTimeline(Context context, String accountID, Hashtag hashtag){ + if(checkIfAlreadyDisplayingSameHashtag(context, hashtag.name)) + return; Bundle args=new Bundle(); args.putString("account", accountID); args.putParcelable("hashtag", Parcels.wrap(hashtag)); @@ -375,12 +378,22 @@ public class UiUtils{ } public static void openHashtagTimeline(Context context, String accountID, String hashtag){ + if(checkIfAlreadyDisplayingSameHashtag(context, hashtag)) + return; Bundle args=new Bundle(); args.putString("account", accountID); args.putString("hashtagName", hashtag); Nav.go((Activity)context, HashtagTimelineFragment.class, args); } + private static boolean checkIfAlreadyDisplayingSameHashtag(Context context, String hashtag){ + if(context instanceof MainActivity ma && ma.getTopmostFragment() instanceof HashtagTimelineFragment htf && htf.getHashtagName().equalsIgnoreCase(hashtag)){ + htf.shakeListView(); + return true; + } + return false; + } + public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){ showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), onConfirmed); } @@ -796,6 +809,10 @@ public class UiUtils{ return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui")); } + public static boolean isMagic() { + return !TextUtils.isEmpty(getSystemProperty("ro.build.version.magic")); + } + public static int alphaBlendColors(int color1, int color2, float alpha){ float alpha0=1f-alpha; int r=Math.round(((color1 >> 16) & 0xFF)*alpha0+((color2 >> 16) & 0xFF)*alpha); @@ -1040,4 +1057,20 @@ public class UiUtils{ button.setTextColor(origColor); } } + + public static void updateRecyclerViewKeepingAbsoluteScrollPosition(RecyclerView rv, Runnable onUpdate){ + int topItem=-1; + int topItemOffset=0; + if(rv.getChildCount()>0){ + View item=rv.getChildAt(0); + topItem=rv.getChildAdapterPosition(item); + topItemOffset=item.getTop(); + } + onUpdate.run(); + int newCount=rv.getAdapter().getItemCount(); + if(newCount>=topItem){ + rv.scrollToPosition(topItem); + rv.scrollBy(0, -topItemOffset); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java index 520ef51e..e2e2b73e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ToolbarDropdownMenuController.java @@ -10,6 +10,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.os.Build; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; @@ -19,6 +20,7 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.Toolbar; +import android.window.OnBackInvokedDispatcher; import org.joinmastodon.android.R; import org.joinmastodon.android.ui.OutlineProviders; @@ -83,6 +85,15 @@ public class ToolbarDropdownMenuController{ .withLayer() .start(); controllerStack.add(initialSubmenu); + + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){ + windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->{ + if(controllerStack.size()>1) + popSubmenuController(); + else + dismiss(); + }); + } } public void dismiss(){ @@ -243,7 +254,7 @@ public class ToolbarDropdownMenuController{ @Override public boolean dispatchKeyEvent(KeyEvent event){ - if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){ + if(Build.VERSION.SDK_INT1) popSubmenuController(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index cec3f1af..8d230ce1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -158,6 +158,9 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie }else{ transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); } + int labelX=label.getLeft(); + int editX=edit.getLeft()+edit.getPaddingLeft(); + float xOffset=editX-labelX; AnimatorSet anim=new AnimatorSet(); if(hintVisible){ @@ -166,6 +169,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie ObjectAnimator.ofFloat(label, SCALE_X, scale), ObjectAnimator.ofFloat(label, SCALE_Y, scale), ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY), + ObjectAnimator.ofFloat(label, TRANSLATION_X, xOffset), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f) ); edit.setHintTextColor(0); @@ -173,11 +177,13 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie label.setScaleX(scale); label.setScaleY(scale); label.setTranslationY(transY); + label.setTranslationX(xOffset); anim.playTogether( ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY), ObjectAnimator.ofFloat(label, SCALE_X, 1f), ObjectAnimator.ofFloat(label, SCALE_Y, 1f), ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f), + ObjectAnimator.ofFloat(label, TRANSLATION_X, 0f), ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f) ); } diff --git a/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml new file mode 100644 index 00000000..f95ac660 --- /dev/null +++ b/mastodon/src/main/res/color/m3_on_surface_variant_alpha40.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_explore_foreground.xml b/mastodon/src/main/res/drawable/ic_explore_foreground.xml new file mode 100644 index 00000000..8b28b47b --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_explore_foreground.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/mastodon/src/main/res/layout/alert_invite_link.xml b/mastodon/src/main/res/layout/alert_invite_link.xml index a188985b..8b1f9d43 100644 --- a/mastodon/src/main/res/layout/alert_invite_link.xml +++ b/mastodon/src/main/res/layout/alert_invite_link.xml @@ -39,14 +39,15 @@ android:layout_height="56dp" android:background="@drawable/bg_m3_filled_text_field" android:paddingHorizontal="16dp" - android:textColorHint="?colorM3OnSurfaceVariant" + android:textColorHint="@color/m3_on_surface_variant_alpha40" android:textColor="?colorM3OnSurface" android:gravity="start|bottom" android:paddingBottom="8dp" android:singleLine="true" android:inputType="textUri" android:textAppearance="@style/m3_body_large" - android:hint="example.social/invite/AbC123"/> + android:hint="example.social/invite/AbC123" + tools:ignore="HardcodedText" /> + android:foreground="@drawable/bg_m3_outlined_text_field" + tools:ignore="RtlHardcoded"> + + - - - @@ -38,7 +38,7 @@ android:textColorHint="?colorM3OnSurfaceVariant" android:inputType="textUri" android:importantForAutofill="no" - android:paddingStart="48dp" + android:paddingStart="64dp" android:layout_marginEnd="52dp" android:drawablePadding="16dp" android:textAppearance="@style/m3_body_large" @@ -47,6 +47,7 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 61cf2fd8..a61ce3aa 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -157,7 +157,6 @@ Email Password Confirm password - Include capital letters, special characters, and numbers to increase your password strength. General Check your inbox @@ -321,9 +320,9 @@ Log in with the server where you created your account. Server URL Any language - Instant sign-up + Instant sign up Manual review - Any sign-up speed + Any sign up speed Europe North America South America @@ -759,6 +758,14 @@ The request timed out. Check your connection and try again? Something went wrong talking with your server. It’s probably not your fault. Try again? It could’ve been deleted, or maybe it never existed at all. + No servers found for “%s” + This username is taken. Try a different one or <a>pick a different server</a>. + That doesn’t look like a valid email address. + Email address is already in use. Did you <a>forget your password</a>? + + %,d new notification + %,d new notifications + Dismiss Just once Monthly diff --git a/mastodon/src/main/res/xml/network_security_config.xml b/mastodon/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..cfac8d81 --- /dev/null +++ b/mastodon/src/main/res/xml/network_security_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file