Merge branch 'master' into donations
# Conflicts: # mastodon/build.gradle # mastodon/src/main/res/values/strings.xml
This commit is contained in:
commit
04129920eb
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(){
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -275,4 +275,8 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
|||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
public String getHashtagName(){
|
||||
return hashtagName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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("");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(){
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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. It’s probably not your fault. Try again?</string>
|
||||
<string name="not_found">It could’ve 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 <a>pick a different server</a>.</string>
|
||||
<string name="signup_email_invalid">That doesn’t look like a valid email address.</string>
|
||||
<string name="signup_email_taken">Email address is already in use. Did you <a>forget your password</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>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue