Settings and other things
This commit is contained in:
@@ -2,7 +2,6 @@ package org.joinmastodon.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
@@ -14,11 +13,11 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
@@ -26,6 +25,7 @@ import me.grishka.appkit.FragmentStackActivity;
|
||||
public class ExternalShareActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean trueBlackTheme;
|
||||
public static ThemePreference theme;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static void load(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.apply();
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -25,6 +26,7 @@ import me.grishka.appkit.FragmentStackActivity;
|
||||
public class MainActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
|
||||
@@ -27,5 +27,6 @@ public class MastodonApp extends Application{
|
||||
context=getApplicationContext();
|
||||
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -24,6 +25,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
public class OAuthActivity extends Activity{
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
Uri uri=getIntent().getData();
|
||||
if(uri==null || isTaskRoot()){
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
@@ -14,8 +13,6 @@ import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -26,7 +23,6 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.WorkerThread;
|
||||
@@ -87,7 +83,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage())));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
@@ -145,7 +141,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage())));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class MastodonAPIController{
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
req.onError(e.getLocalizedMessage());
|
||||
req.onError(e.getLocalizedMessage(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,7 +136,7 @@ public class MastodonAPIController{
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ public class MastodonAPIController{
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ public class MastodonAPIController{
|
||||
try{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
req.onError(error.get("error").getAsString());
|
||||
req.onError(error.get("error").getAsString(), response.code());
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
req.onError(response.code()+" "+response.message());
|
||||
req.onError(response.code()+" "+response.message(), response.code());
|
||||
}catch(IllegalStateException x){
|
||||
req.onError("Error parsing an API error");
|
||||
req.onError("Error parsing an API error", response.code());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +170,7 @@ public class MastodonAPIController{
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage());
|
||||
req.onError(x.getLocalizedMessage(), 0);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package org.joinmastodon.android.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
@@ -60,6 +60,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
|
||||
@Override
|
||||
public synchronized void cancel(){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "canceling request "+this);
|
||||
canceled=true;
|
||||
if(okhttpCall!=null){
|
||||
okhttpCall.cancel();
|
||||
@@ -181,8 +183,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
}
|
||||
|
||||
void onError(String msg){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg));
|
||||
void onError(String msg, int httpStatus){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
|
||||
}
|
||||
|
||||
void onSuccess(T resp){
|
||||
|
||||
@@ -11,9 +11,11 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
public final String error;
|
||||
public final int httpStatus;
|
||||
|
||||
public MastodonErrorResponse(String error){
|
||||
public MastodonErrorResponse(String error, int httpStatus){
|
||||
this.error=error;
|
||||
this.httpStatus=httpStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.util.Log;
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.notifications.RegisterForPushNotifications;
|
||||
import org.joinmastodon.android.api.requests.notifications.UpdatePushSettings;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
@@ -119,6 +120,10 @@ public class PushSubscriptionManager{
|
||||
}
|
||||
|
||||
public void registerAccountForPush(){
|
||||
registerAccountForPush(null);
|
||||
}
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription){
|
||||
if(TextUtils.isEmpty(deviceToken))
|
||||
throw new IllegalStateException("No device push token available");
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
@@ -143,19 +148,22 @@ public class PushSubscriptionManager{
|
||||
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
||||
return;
|
||||
}
|
||||
new RegisterForPushNotifications(deviceToken, encodedPublicKey, encodedAuthKey, PushSubscription.Alerts.ofAll(), accountID)
|
||||
new RegisterForPushNotifications(deviceToken,
|
||||
encodedPublicKey,
|
||||
encodedAuthKey,
|
||||
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
|
||||
subscription==null ? PushSubscription.Policy.ALL : subscription.policy,
|
||||
accountID)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
||||
|
||||
if(serverKey!=null){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
session.pushServerKey=Base64.encodeToString(serverKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
|
||||
}
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
session.pushSubscription=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,6 +176,34 @@ public class PushSubscriptionManager{
|
||||
});
|
||||
}
|
||||
|
||||
public void updatePushSettings(PushSubscription subscription){
|
||||
new UpdatePushSettings(subscription.alerts, subscription.policy)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(result.policy!=subscription.policy)
|
||||
result.policy=subscription.policy;
|
||||
session.pushSubscription=result;
|
||||
session.needUpdatePushSettings=false;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(((MastodonErrorResponse)error).httpStatus==404){ // Not registered for push, register now
|
||||
registerAccountForPush(subscription);
|
||||
}else{
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
session.needUpdatePushSettings=true;
|
||||
session.pushSubscription=subscription;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private PublicKey deserializeRawPublicKey(byte[] rawBytes){
|
||||
if(rawBytes.length!=65 && rawBytes.length!=64)
|
||||
return null;
|
||||
@@ -320,8 +356,10 @@ public class PushSubscriptionManager{
|
||||
|
||||
private static void registerAllAccountsForPush(){
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
if(TextUtils.isEmpty(session.pushServerKey))
|
||||
if(session.pushSubscription==null)
|
||||
session.getPushSubscriptionManager().registerAccountForPush();
|
||||
else if(session.needUpdatePushSettings)
|
||||
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
|
||||
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, String accountID){
|
||||
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy, String accountID){
|
||||
super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
|
||||
Request r=new Request();
|
||||
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
r.data.alerts=alerts;
|
||||
r.data.policy=policy;
|
||||
r.subscription.keys.p256dh=encryptionKey;
|
||||
r.subscription.keys.auth=authKey;
|
||||
setRequestBody(r);
|
||||
@@ -30,6 +31,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
|
||||
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
|
||||
setRequestBody(new Request(alerts, policy));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Data data=new Data();
|
||||
|
||||
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
this.data.alerts=alerts;
|
||||
this.data.policy=policy;
|
||||
}
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class AccountSession{
|
||||
@@ -19,7 +19,8 @@ public class AccountSession{
|
||||
public String pushPrivateKey;
|
||||
public String pushPublicKey;
|
||||
public String pushAuthKey;
|
||||
public String pushServerKey;
|
||||
public PushSubscription pushSubscription;
|
||||
public boolean needUpdatePushSettings;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController;
|
||||
private transient CacheController cacheController;
|
||||
|
||||
@@ -9,8 +9,10 @@ import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -592,6 +594,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return list.getWidth();
|
||||
}
|
||||
|
||||
protected boolean wantsOverlaySystemNavigation(){
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
|
||||
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
onSetFabBottomInset(insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
onSetFabBottomInset(0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
|
||||
@@ -993,32 +993,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
||||
PopupMenu menu=new PopupMenu(getActivity(), v);
|
||||
menu.inflate(R.menu.compose_visibility);
|
||||
Menu m=menu.getMenu();
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
menu.setForceShowIcon(true);
|
||||
}else{
|
||||
try{
|
||||
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
|
||||
setOptionalIconsVisible.setAccessible(true);
|
||||
setOptionalIconsVisible.invoke(m, true);
|
||||
}catch(Exception ignore){}
|
||||
}
|
||||
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
|
||||
for(int i=0;i<m.size();i++){
|
||||
MenuItem item=m.getItem(i);
|
||||
Drawable icon=item.getIcon().mutate();
|
||||
if(Build.VERSION.SDK_INT>=26){
|
||||
item.setIconTintList(iconTint);
|
||||
}else{
|
||||
icon.setTintList(iconTint);
|
||||
}
|
||||
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
|
||||
item.setIcon(icon);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
|
||||
ssb.insert(0, " ");
|
||||
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
|
||||
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
|
||||
item.setTitle(ssb);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
||||
m.setGroupCheckable(0, true, true);
|
||||
m.findItem(switch(statusVisibility){
|
||||
case PUBLIC, UNLISTED -> R.id.vis_public;
|
||||
@@ -1113,6 +1088,16 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
|
||||
finishAutocomplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Parcel
|
||||
static class DraftMediaAttachment{
|
||||
public Attachment serverAttachment;
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -13,6 +14,7 @@ import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends StatusListFragment{
|
||||
private String hashtag;
|
||||
@@ -61,4 +63,9 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
args.putString("prefilledText", '#'+hashtag+' ');
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetFabBottomInset(int inset){
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -16,18 +18,28 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
@@ -58,21 +70,23 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTimelineFragment=new HomeTimelineFragment();
|
||||
homeTimelineFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
notificationsFragment=new NotificationsFragment();
|
||||
notificationsFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
profileFragment=new ProfileFragment();
|
||||
profileFragment.setArguments(args);
|
||||
if(savedInstanceState==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTimelineFragment=new HomeTimelineFragment();
|
||||
homeTimelineFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
searchFragment=new DiscoverFragment();
|
||||
searchFragment.setArguments(args);
|
||||
notificationsFragment=new NotificationsFragment();
|
||||
notificationsFragment.setArguments(args);
|
||||
args=new Bundle(args);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
profileFragment=new ProfileFragment();
|
||||
profileFragment.setArguments(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,7 +102,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
inflater.inflate(R.layout.tab_bar, content);
|
||||
tabBar=content.findViewById(R.id.tabbar);
|
||||
tabBar.setListener(this::onTabSelected);
|
||||
tabBar.setListeners(this::onTabSelected, this::onTabLongClick);
|
||||
tabBarWrap=content.findViewById(R.id.tabbar_wrap);
|
||||
|
||||
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
|
||||
@@ -123,12 +137,32 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
});
|
||||
}
|
||||
}else{
|
||||
tabBar.selectTab(currentTab);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(Bundle savedInstanceState){
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if(savedInstanceState==null || homeTimelineFragment!=null)
|
||||
return;
|
||||
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
|
||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||
currentTab=savedInstanceState.getInt("selectedTab");
|
||||
Fragment current=fragmentForTab(currentTab);
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.hide(homeTimelineFragment)
|
||||
.hide(searchFragment)
|
||||
.hide(notificationsFragment)
|
||||
.hide(profileFragment)
|
||||
.show(current)
|
||||
.commit();
|
||||
maybeTriggerLoading(current);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenChanged(boolean hidden){
|
||||
super.onHiddenChanged(hidden);
|
||||
@@ -137,12 +171,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return currentTab!=R.id.tab_profile && (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES;
|
||||
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES;
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -182,6 +216,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
return;
|
||||
}
|
||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||
maybeTriggerLoading(newFragment);
|
||||
currentTab=tab;
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
}
|
||||
|
||||
private void maybeTriggerLoading(Fragment newFragment){
|
||||
if(newFragment instanceof LoaderFragment){
|
||||
LoaderFragment lf=(LoaderFragment) newFragment;
|
||||
if(!lf.loaded && !lf.dataLoading)
|
||||
@@ -194,8 +234,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
||||
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
|
||||
}
|
||||
currentTab=tab;
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
}
|
||||
|
||||
private boolean onTabLongClick(@IdRes int tab){
|
||||
if(tab==R.id.tab_profile){
|
||||
ArrayList<String> options=new ArrayList<>();
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setItems(options.toArray(new String[0]), (dialog, which)->{
|
||||
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
})
|
||||
.setNegativeButton(R.string.add_account, (dialog, which)->{
|
||||
Nav.go(getActivity(), SplashFragment.class, null);
|
||||
})
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,4 +266,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
return searchFragment.onBackPressed();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("selectedTab", currentTab);
|
||||
getChildFragmentManager().putFragment(outState, "homeTimelineFragment", homeTimelineFragment);
|
||||
getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
|
||||
getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
|
||||
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,42 +83,9 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
ArrayList<String> options=new ArrayList<>();
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setItems(options.toArray(new String[0]), (dialog, which)->{
|
||||
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
})
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->{
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(session.getID());
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(session.getID());
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.execNoAuth(session.domain);
|
||||
})
|
||||
.setNegativeButton(R.string.add_account, (dialog, which)->{
|
||||
Nav.go(getActivity(), SplashFragment.class, null);
|
||||
})
|
||||
.show();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -34,6 +36,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
@@ -44,7 +47,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ProfileAboutFragment extends Fragment{
|
||||
public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{
|
||||
private static final int MAX_FIELDS=4;
|
||||
|
||||
public UsableRecyclerView list;
|
||||
@@ -111,6 +114,23 @@ public class ProfileAboutFragment extends Fragment{
|
||||
return fields;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return false;
|
||||
}
|
||||
|
||||
private class AboutAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AboutAdapter(){
|
||||
super(imgLoader);
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
@@ -115,6 +116,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private String profileAccountID;
|
||||
private boolean refreshing;
|
||||
private View fab;
|
||||
private WindowInsets childInsets;
|
||||
|
||||
public ProfileFragment(){
|
||||
super(R.layout.loader_fragment_overlay_toolbar);
|
||||
@@ -365,16 +367,35 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
statusBarHeight=insets.getSystemWindowInsetTop();
|
||||
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=statusBarHeight;
|
||||
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
|
||||
if(contentView!=null){
|
||||
((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin=statusBarHeight;
|
||||
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+insetBottom;
|
||||
applyChildWindowInsets();
|
||||
insets=insets.inset(0, 0, 0, insetBottom);
|
||||
}else{
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24);
|
||||
}
|
||||
}
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
|
||||
private void applyChildWindowInsets(){
|
||||
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
||||
postsFragment.onApplyWindowInsets(childInsets);
|
||||
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
||||
mediaFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindHeaderView(){
|
||||
setTitle(account.displayName);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(account.avatar, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(account.header, 1000, 1000));
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
@@ -790,8 +811,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
|
||||
Fragment fragment=getFragmentForPage(position);
|
||||
if(!fragment.isAdded())
|
||||
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), getFragmentForPage(position)).commit();
|
||||
if(!fragment.isAdded()){
|
||||
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
|
||||
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
if(fragment.isAdded()){
|
||||
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
applyChildWindowInsets();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SettingsFragment extends ToolbarFragment{
|
||||
private UsableRecyclerView list;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.settings);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
GlobalUserPreferences.playGifs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
|
||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
PushSubscription pushSubscription=getPushSubscription();
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_boring));
|
||||
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
list=new UsableRecyclerView(getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// Add 32dp gaps between sections
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(needUpdateNotificationSettings){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||
GlobalUserPreferences.theme=theme;
|
||||
GlobalUserPreferences.save();
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
|
||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||
GlobalUserPreferences.save();
|
||||
|
||||
RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
|
||||
if(themeHolder!=null){
|
||||
((ThemeViewHolder)themeHolder).bindSubitems();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
|
||||
}
|
||||
|
||||
if(UiUtils.isDarkTheme()){
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private void restartActivityToApplyNewTheme(){
|
||||
// Calling activity.recreate() causes a black screen for like half a second.
|
||||
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
|
||||
// As a bonus, we can fade it out to make it even smoother.
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
View activityDecorView=getActivity().getWindow().getDecorView();
|
||||
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
activityDecorView.draw(new Canvas(bitmap));
|
||||
themeTransitionWindowView=new ImageView(MastodonApp.context);
|
||||
themeTransitionWindowView.setImageBitmap(bitmap);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
|
||||
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
|
||||
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
|
||||
lp.token=getActivity().getWindow().getAttributes().token;
|
||||
lp.windowAnimations=R.style.window_fade_out;
|
||||
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
|
||||
}
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
private PushSubscription getPushSubscription(){
|
||||
if(pushSubscription!=null)
|
||||
return pushSubscription;
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(session.pushSubscription==null){
|
||||
pushSubscription=new PushSubscription();
|
||||
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
|
||||
}else{
|
||||
pushSubscription=session.pushSubscription.clone();
|
||||
}
|
||||
return pushSubscription;
|
||||
}
|
||||
|
||||
private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
switch(type){
|
||||
case FAVORITE -> subscription.alerts.favourite=enabled;
|
||||
case FOLLOW -> subscription.alerts.follow=enabled;
|
||||
case REBLOG -> subscription.alerts.reblog=enabled;
|
||||
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
PushSubscription.Policy prevPolicy=subscription.policy;
|
||||
if(prevPolicy==policy)
|
||||
return;
|
||||
subscription.policy=policy;
|
||||
int index=items.indexOf(notificationPolicyItem);
|
||||
RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
|
||||
if(policyHolder!=null){
|
||||
((NotificationPolicyViewHolder)policyHolder).rebind();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
}
|
||||
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
|
||||
index++;
|
||||
while(items.get(index) instanceof SwitchItem){
|
||||
SwitchItem si=(SwitchItem) items.get(index);
|
||||
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
|
||||
if(holder!=null)
|
||||
((BindableViewHolder<?>)holder).rebind();
|
||||
else
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void confirmLogOut(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(R.string.confirm_log_out)
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void logOut(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut();
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
getActivity().finish();
|
||||
}
|
||||
|
||||
private void clearImageCache(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
|
||||
private class HeaderItem extends Item{
|
||||
private String text;
|
||||
|
||||
public HeaderItem(@StringRes int text){
|
||||
this.text=getString(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchItem extends Item{
|
||||
private String text;
|
||||
private int icon;
|
||||
private boolean checked;
|
||||
private Consumer<SwitchItem> onChanged;
|
||||
private boolean enabled=true;
|
||||
|
||||
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
}
|
||||
|
||||
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
this.enabled=enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThemeItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NotificationPolicyItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
private class RedHeaderItem extends HeaderItem{
|
||||
|
||||
public RedHeaderItem(int text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterItem extends Item{
|
||||
private String text;
|
||||
|
||||
public FooterItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<Item> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
//noinspection unchecked
|
||||
return (BindableViewHolder<Item>) switch(viewType){
|
||||
case 0 -> new HeaderViewHolder(false);
|
||||
case 1 -> new SwitchViewHolder();
|
||||
case 2 -> new ThemeViewHolder();
|
||||
case 3 -> new NotificationPolicyViewHolder();
|
||||
case 4 -> new TextViewHolder();
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BindableViewHolder<Item> holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getViewType();
|
||||
}
|
||||
}
|
||||
|
||||
private class HeaderViewHolder extends BindableViewHolder<HeaderItem>{
|
||||
private final TextView text;
|
||||
public HeaderViewHolder(boolean red){
|
||||
super(getActivity(), R.layout.item_settings_header, list);
|
||||
text=(TextView) itemView;
|
||||
if(red)
|
||||
text.setTextColor(getResources().getColor(R.color.error_700));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchViewHolder extends BindableViewHolder<SwitchItem> implements UsableRecyclerView.DisableableClickable{
|
||||
private final TextView text;
|
||||
private final ImageView icon;
|
||||
private final Switch checkbox;
|
||||
|
||||
public SwitchViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_switch, list);
|
||||
text=findViewById(R.id.text);
|
||||
icon=findViewById(R.id.icon);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SwitchItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
checkbox.setChecked(item.checked && item.enabled);
|
||||
checkbox.setEnabled(item.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.checked=!item.checked;
|
||||
checkbox.setChecked(item.checked);
|
||||
item.onChanged.accept(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private class ThemeViewHolder extends BindableViewHolder<ThemeItem>{
|
||||
private SubitemHolder autoHolder, lightHolder, darkHolder;
|
||||
|
||||
public ThemeViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_theme, list);
|
||||
autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
|
||||
lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
|
||||
darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ThemeItem item){
|
||||
bindSubitems();
|
||||
}
|
||||
|
||||
public void bindSubitems(){
|
||||
autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
|
||||
lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
|
||||
darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
|
||||
}
|
||||
|
||||
private void onSubitemClick(View v){
|
||||
GlobalUserPreferences.ThemePreference pref;
|
||||
if(v.getId()==R.id.theme_auto)
|
||||
pref=GlobalUserPreferences.ThemePreference.AUTO;
|
||||
else if(v.getId()==R.id.theme_light)
|
||||
pref=GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
else if(v.getId()==R.id.theme_dark)
|
||||
pref=GlobalUserPreferences.ThemePreference.DARK;
|
||||
else
|
||||
return;
|
||||
onThemePreferenceClick(pref);
|
||||
}
|
||||
|
||||
private class SubitemHolder{
|
||||
public TextView text;
|
||||
public ImageView icon, checkbox;
|
||||
|
||||
public SubitemHolder(View view){
|
||||
text=view.findViewById(R.id.text);
|
||||
icon=view.findViewById(R.id.icon);
|
||||
checkbox=view.findViewById(R.id.checkbox);
|
||||
view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
|
||||
|
||||
icon.setClipToOutline(true);
|
||||
icon.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
}
|
||||
|
||||
public void bind(int text, int icon, boolean checked){
|
||||
this.text.setText(text);
|
||||
this.icon.setImageResource(icon);
|
||||
checkbox.setSelected(checked);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
checkbox.setSelected(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
|
||||
private final Button button;
|
||||
private final PopupMenu popupMenu;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public NotificationPolicyViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_notification_policy, list);
|
||||
button=findViewById(R.id.button);
|
||||
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.notification_policy);
|
||||
popupMenu.setOnMenuItemClickListener(item->{
|
||||
PushSubscription.Policy policy;
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.notify_anyone)
|
||||
policy=PushSubscription.Policy.ALL;
|
||||
else if(id==R.id.notify_followed)
|
||||
policy=PushSubscription.Policy.FOLLOWED;
|
||||
else if(id==R.id.notify_follower)
|
||||
policy=PushSubscription.Policy.FOLLOWER;
|
||||
else if(id==R.id.notify_none)
|
||||
policy=PushSubscription.Policy.NONE;
|
||||
else
|
||||
return false;
|
||||
onNotificationsPolicyChanged(policy);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(NotificationPolicyItem item){
|
||||
button.setText(switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notify_anyone;
|
||||
case FOLLOWED -> R.string.notify_followed;
|
||||
case FOLLOWER -> R.string.notify_follower;
|
||||
case NONE -> R.string.notify_none;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
|
||||
private final TextView text;
|
||||
public FooterViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_footer, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FooterItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class AccountActivationFragment extends AppKitFragment{
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES;
|
||||
return !UiUtils.isDarkTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -281,4 +281,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
if(ev.reportAccountID.equals(reportAccount.id))
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsOverlaySystemNavigation(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class PushSubscription extends BaseModel{
|
||||
public class PushSubscription extends BaseModel implements Cloneable{
|
||||
public int id;
|
||||
public String endpoint;
|
||||
public Alerts alerts;
|
||||
public String serverKey;
|
||||
public Policy policy=Policy.ALL;
|
||||
|
||||
public PushSubscription(){}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
@@ -19,7 +26,18 @@ public class PushSubscription extends BaseModel{
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class Alerts{
|
||||
@NonNull
|
||||
@Override
|
||||
public PushSubscription clone(){
|
||||
PushSubscription copy=null;
|
||||
try{
|
||||
copy=(PushSubscription) super.clone();
|
||||
}catch(CloneNotSupportedException ignore){}
|
||||
copy.alerts=alerts.clone();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static class Alerts implements Cloneable{
|
||||
public boolean follow;
|
||||
public boolean favourite;
|
||||
public boolean reblog;
|
||||
@@ -42,5 +60,26 @@ public class PushSubscription extends BaseModel{
|
||||
", poll="+poll+
|
||||
'}';
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Alerts clone(){
|
||||
try{
|
||||
return (Alerts) super.clone();
|
||||
}catch(CloneNotSupportedException e){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Policy{
|
||||
@SerializedName("all")
|
||||
ALL,
|
||||
@SerializedName("followed")
|
||||
FOLLOWED,
|
||||
@SerializedName("follower")
|
||||
FOLLOWER,
|
||||
@SerializedName("none")
|
||||
NONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
@@ -52,7 +53,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
super(parentID, parentFragment);
|
||||
this.user=user;
|
||||
this.createdAt=createdAt;
|
||||
avaRequest=new UrlImageLoaderRequest(user.avatar, V.dp(50), V.dp(50));
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
|
||||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
this.status=status;
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -53,6 +54,6 @@ public class CustomEmojiSpan extends ReplacementSpan{
|
||||
|
||||
public UrlImageLoaderRequest createImageLoaderRequest(){
|
||||
int size=V.dp(20);
|
||||
return new UrlImageLoaderRequest(emoji.url, size, size);
|
||||
return new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? emoji.url : emoji.staticUrl, size, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,34 @@ package org.joinmastodon.android.ui.utils;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.InsetDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
||||
@@ -36,8 +46,10 @@ import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -69,11 +81,14 @@ public class UiUtils{
|
||||
private UiUtils(){}
|
||||
|
||||
public static void launchWebBrowser(Context context, String url){
|
||||
// TODO setting for custom tabs
|
||||
new CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse(url));
|
||||
if(GlobalUserPreferences.useCustomTabs){
|
||||
new CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse(url));
|
||||
}else{
|
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatRelativeTimestamp(Context context, Instant instant){
|
||||
@@ -374,4 +389,48 @@ public class UiUtils{
|
||||
d.draw(new Canvas(bitmap));
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
|
||||
Menu m=menu.getMenu();
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
menu.setForceShowIcon(true);
|
||||
}else{
|
||||
try{
|
||||
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
|
||||
setOptionalIconsVisible.setAccessible(true);
|
||||
setOptionalIconsVisible.invoke(m, true);
|
||||
}catch(Exception ignore){}
|
||||
}
|
||||
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
|
||||
for(int i=0;i<m.size();i++){
|
||||
MenuItem item=m.getItem(i);
|
||||
Drawable icon=item.getIcon().mutate();
|
||||
if(Build.VERSION.SDK_INT>=26){
|
||||
item.setIconTintList(iconTint);
|
||||
}else{
|
||||
icon.setTintList(iconTint);
|
||||
}
|
||||
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
|
||||
item.setIcon(icon);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
|
||||
ssb.insert(0, " ");
|
||||
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
|
||||
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
|
||||
item.setTitle(ssb);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setUserPreferredTheme(Context context){
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||
case LIGHT -> R.style.Theme_Mastodon_Light;
|
||||
case DARK -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme(){
|
||||
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
|
||||
return GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.LinearLayout;
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -17,6 +18,7 @@ public class TabBar extends LinearLayout{
|
||||
@IdRes
|
||||
private int selectedTabID;
|
||||
private IntConsumer listener;
|
||||
private IntPredicate longClickListener;
|
||||
|
||||
public TabBar(Context context){
|
||||
this(context, null);
|
||||
@@ -39,6 +41,7 @@ public class TabBar extends LinearLayout{
|
||||
child.setSelected(true);
|
||||
}
|
||||
child.setOnClickListener(this::onChildClick);
|
||||
child.setOnLongClickListener(this::onChildLongClick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +54,13 @@ public class TabBar extends LinearLayout{
|
||||
selectedTabID=v.getId();
|
||||
}
|
||||
|
||||
public void setListener(IntConsumer listener){
|
||||
private boolean onChildLongClick(View v){
|
||||
return longClickListener.test(v.getId());
|
||||
}
|
||||
|
||||
public void setListeners(IntConsumer listener, IntPredicate longClickListener){
|
||||
this.listener=listener;
|
||||
this.longClickListener=longClickListener;
|
||||
}
|
||||
|
||||
public void selectTab(int id){
|
||||
|
||||
Reference in New Issue
Block a user