Merge branch 'master' into donations

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/res/values/strings.xml
This commit is contained in:
Grishka 2024-06-13 16:57:23 +03:00
commit 04129920eb
39 changed files with 705 additions and 277 deletions

View File

@ -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'

View File

@ -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">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
@ -79,6 +81,7 @@
</activity>
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
<service android:name=".NotificationActionHandlerService" android:exported="false"/>
<receiver android:name=".PushNotificationReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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<String> 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<String> 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());
}
}
}

View File

@ -13,11 +13,13 @@ import java.util.List;
public class GetCatalogInstances extends MastodonAPIRequest<List<CatalogInstance>>{
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<List<CatalogInstance
builder.appendQueryParameter("language", lang);
if(!TextUtils.isEmpty(category))
builder.appendQueryParameter("category", category);
if(includeClosedSignups)
builder.appendQueryParameter("registrations", "all");
return builder.build();
}
}

View File

@ -436,7 +436,7 @@ public class AccountSessionManager{
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
ShortcutInfo compose=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
@ -444,12 +444,20 @@ public class AccountSessionManager{
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
ShortcutInfo explore=new ShortcutInfo.Builder(MastodonApp.context, "explore")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.tab_search))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_explore))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("explore", true))
.build();
sm.setDynamicShortcuts(List.of(compose, explore));
}else if(sessions.isEmpty()){
// There are shortcuts, but no accounts. Disable existing shortcuts.
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
sm.disableShortcuts(List.of("compose", "explore"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
sm.enableShortcuts(List.of("compose", "explore"));
}
}

View File

@ -59,6 +59,9 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@ -79,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
private SpringAnimation listShakeAnimation;
public BaseStatusListFragment(){
super(20);
@ -675,6 +679,17 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> 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<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){

View File

@ -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();
}

View File

@ -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

View File

@ -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<String> 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);

View File

@ -275,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{
})
.exec(accountID);
}
public String getHashtagName(){
return hashtagName;
}
}

View File

@ -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);

View File

@ -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<String> 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);
}

View File

@ -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<Attachment> 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)));

View File

@ -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

View File

@ -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){

View File

@ -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<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> 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<SearchResultVi
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
if(!enter){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
}
return set;
}
@ -437,14 +441,6 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;

View File

@ -73,8 +73,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected boolean isSignup;
protected CatalogInstance fakeInstance=new CatalogInstance();
private static final double DUNBAR=Math.log(800);
public InstanceCatalogFragment(int layout, int perPage){
super(layout, perPage);
}
@ -155,7 +153,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
if(TextUtils.isEmpty(_domain) || _domain.indexOf('.')==-1)
return;
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);

View File

@ -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<CatalogCategory> categories=new ArrayList<>();
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View topBar;
private List<String> 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<CatalogInstance> result){
@ -149,58 +140,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> 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("");

View File

@ -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<CatalogInstance> 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<list.getChildCount();i++){
list.getChildAt(i).invalidateOutline();
@ -90,12 +96,13 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
private void loadAutocompleteServers(){
loadedAutocomplete=true;
new GetCatalogInstances(null, null)
new GetCatalogInstances(null, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> 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

View File

@ -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;

View File

@ -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<Void> implements OnBackPressedListener{
public class EditFilterFragment extends BaseSettingsFragment<Void>{
private static final int WORDS_RESULT=370;
private static final int CONTEXT_RESULT=651;
@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
private ArrayList<String> deletedWordIDs=new ArrayList<>();
private EnumSet<FilterContext> 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<Void> 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<Void> implements On
}
a.dismiss();
}
updateBackCallback();
})
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@ -309,6 +318,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
}
updateBackCallback();
}
}
@ -317,11 +327,19 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> 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;
}
}

View File

@ -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<FilterContext> implements OnBackPressedListener{
public class FilterContextFragment extends BaseSettingsFragment<FilterContext>{
private EnumSet<FilterContext> context;
@Override
@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
protected void doLoadData(int offset, int count){}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
context=EnumSet.noneOf(FilterContext.class);
for(ListItem<FilterContext> item:data){
if(((CheckableListItem<FilterContext>) item).checked)
@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
Bundle args=new Bundle();
args.putSerializable("context", context);
setResult(true, args);
return false;
}
}

View File

@ -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<FilterKeyword> implements OnBackPressedListener{
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword>{
private Button fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> 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<FilterKeyword> imp
}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
Bundle result=new Bundle();
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
result.putStringArrayList("deleted", deletedItemIDs);
setResult(true, result);
return false;
}
@Override
@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
updateActionModeTitle();
addBackCallback(actionModeDismisser);
}
private void leaveSelectionMode(boolean fromActionMode){
@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
data.set(i, newItem);
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
removeBackCallback(actionModeDismisser);
}
private void updateActionModeTitle(){

View File

@ -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<String> languages;
@SerializedName("region")
private String _region;
@RequiredField
public List<String> categories;
public String proxiedThumbnail;
public int totalUsers;

View File

@ -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;

View File

@ -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<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
onStartSwipeToDismissTransition(0f);
}
@ -257,6 +258,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
wlp.layoutInDisplayCutoutMode=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

View File

@ -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);
}
}
}

View File

@ -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_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
if(controllerStack.size()>1)
popSubmenuController();

View File

@ -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)
);
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSurfaceVariant" android:alpha="0.4"/>
</selector>

View File

@ -0,0 +1,7 @@
<vector android:height="108dp"
android:viewportHeight="56" android:viewportWidth="56"
android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group android:translateX="16" android:translateY="16">
<path android:fillColor="@color/shortcut_icon_foreground" android:pathData="M19.6,21 L13.3,14.7Q12.55,15.3 11.575,15.65Q10.6,16 9.5,16Q6.775,16 4.888,14.113Q3,12.225 3,9.5Q3,6.775 4.888,4.887Q6.775,3 9.5,3Q12.225,3 14.113,4.887Q16,6.775 16,9.5Q16,10.6 15.65,11.575Q15.3,12.55 14.7,13.3L21,19.6ZM9.5,14Q11.375,14 12.688,12.688Q14,11.375 14,9.5Q14,7.625 12.688,6.312Q11.375,5 9.5,5Q7.625,5 6.312,6.312Q5,7.625 5,9.5Q5,11.375 6.312,12.688Q7.625,14 9.5,14Z"/>
</group>
</vector>

View File

@ -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" />
<TextView
android:id="@+id/label"

View File

@ -56,10 +56,12 @@
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/username_wrap"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="12dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
android:foreground="@drawable/bg_m3_outlined_text_field"
tools:ignore="RtlHardcoded">
<EditText
android:id="@+id/username"
@ -91,6 +93,20 @@
android:textColor="?colorM3OnSurface"
tools:text="\@mastodon.social"/>
<TextView
android:id="@+id/at_sign"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_gravity="left|top"
android:layout_marginLeft="56dp"
android:layout_marginTop="8dp"
android:paddingLeft="8dp"
android:paddingRight="2dp"
android:textAppearance="@style/m3_body_large"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurface"
android:text="\@"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
@ -191,19 +207,6 @@
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="-8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/password_note"/>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/reason_wrap"
android:layout_width="match_parent"

View File

@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_body_large"
android:paddingStart="56dp"
android:paddingStart="80dp"
android:paddingEnd="24dp"
android:textColor="?colorM3OnSurface"
android:text="@string/login_subtitle"/>
@ -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 @@
<ImageView
android:layout_width="44dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:layout_gravity="start|center_vertical"
android:scaleType="center"
android:importantForAccessibility="no"

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<shape>
<solid android:color="@color/shortcut_icon_background"/>
<size android:width="108dp" android:height="108dp"/>
</shape>
</background>
<foreground android:drawable="@drawable/ic_explore_foreground"/>
</adaptive-icon>

View File

@ -157,7 +157,6 @@
<string name="email">Email</string>
<string name="password">Password</string>
<string name="confirm_password">Confirm password</string>
<string name="password_note">Include capital letters, special characters, and numbers to increase your password strength.</string>
<string name="category_general">General</string>
<string name="confirm_email_title">Check your inbox</string>
<!-- %s is the email address -->
@ -321,9 +320,9 @@
<string name="login_subtitle">Log in with the server where you created your account.</string>
<string name="server_url">Server URL</string>
<string name="server_filter_any_language">Any language</string>
<string name="server_filter_instant_signup">Instant sign-up</string>
<string name="server_filter_instant_signup">Instant sign up</string>
<string name="server_filter_manual_review">Manual review</string>
<string name="server_filter_any_signup_speed">Any sign-up speed</string>
<string name="server_filter_any_signup_speed">Any sign up speed</string>
<string name="server_filter_region_europe">Europe</string>
<string name="server_filter_region_north_america">North America</string>
<string name="server_filter_region_south_america">South America</string>
@ -759,6 +758,14 @@
<string name="connection_timed_out">The request timed out. Check your connection and try again?</string>
<string name="server_error">Something went wrong talking with your server. Its probably not your fault. Try again?</string>
<string name="not_found">It couldve been deleted, or maybe it never existed at all.</string>
<string name="no_servers_found">No servers found for “%s”</string>
<string name="signup_username_taken">This username is taken. Try a different one or &lt;a>pick a different server&lt;/a>.</string>
<string name="signup_email_invalid">That doesnt look like a valid email address.</string>
<string name="signup_email_taken">Email address is already in use. Did you &lt;a>forget your password&lt;/a>?</string>
<plurals name="x_new_notifications">
<item quantity="one">%,d new notification</item>
<item quantity="other">%,d new notifications</item>
</plurals>
<string name="dismiss">Dismiss</string>
<string name="donation_once">Just once</string>
<string name="donation_monthly">Monthly</string>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>