Compare commits

..

1 Commits

Author SHA1 Message Date
LucasGGamerM
a168a0226b fix(recycler-empty-view): make the textViews in the empty views follow the color themes 2023-09-25 11:28:28 -03:00
342 changed files with 2627 additions and 14887 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: mastodon
open_collective: # Replace with a single Open Collective username e.g., user1
ko_fi: # Replace with a single Ko-fi username e.g., user1
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username e.g., user1
issuehunt: # Replace with a single IssueHunt username e.g., user1
otechie: # Replace with a single Otechie username e.g., user1
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 KiB

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,16 +1,16 @@
Mastodon इंटरनेट का सबसे बड़ा डिसेंट्रलाइज़्ड सोशल नेटवर्क है। एक सिंगल वेबसाइट के जगह, ये हज़ारों आज़ाद ग्रुपों के लाखों यूज़रों का एक नेटवर्क है जो एक दूसरे से आसानी से बात करते है। चाहे आपकी जो भी दिलचस्पी हो, आपको उसके ऊपर पोस्ट करनेवाले लोग Mastodon पे ज़रूर मिलेंगे! Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
कोई ग्रुप जॉइन करें और अपना प्रोफाइल बनाएं। दिलचस्प लोगों को ढूंढ़ें और फॉलो करें और उनके पोस्ट पढ़ें बिना किसी ऐड के। Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network. Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse. Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
एक्स्ट्रा फीचर: More features:
डार्क मोड: पोस्ट लाइट, डार्क, या प्योर ब्लैक मोड में पढ़ें Dark Mode: Read posts in light, dark, or true black mode
• Polls: Ask followers for their opinion and tally the votes • Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away • Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs • Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app • Sharing: Post directly to Mastodon from any share sheet in any app
क्यूटपन: हमारा मैस्कॉट एक प्यारा हाथी है, और आप उसे समय-समय पे देखेंगे Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way. Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -1 +1 @@
डिसेंट्रलाइज़्ड सोशल नेटवर्क Decentralized social network

View File

@@ -1 +1 @@
Decentralizált közösségi hálózat Decentralizált szociális hálózat

View File

@@ -1,16 +0,0 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
• Dark Mode: Read posts in light, dark, or true black mode
• Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -1 +0,0 @@
Decentralized social network

View File

@@ -1 +0,0 @@
Mastodon

View File

@@ -1,16 +0,0 @@
Mastodon didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
Daugiau funkcijų:
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
• Naršyti: tendencingos saitažodžiai ir paskyros vos nuo vieno prisilietimo
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.

View File

@@ -1 +0,0 @@
Decentralizuotas socialinis tinklas

View File

@@ -1 +0,0 @@
Mastodon

View File

@@ -9,10 +9,10 @@ android {
applicationId "org.joinmastodon.android" applicationId "org.joinmastodon.android"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 84 versionCode 66
versionName "2.3.0" versionName "2.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW" resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
} }
buildTypes { buildTypes {
@@ -76,7 +76,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0' implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.16' implementation 'me.grishka.appkit:appkit:1.2.10'
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8' implementation 'com.squareup:otto:1.3.8'

View File

@@ -3,9 +3,6 @@ package org.joinmastodon.android;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
public class GlobalUserPreferences{ public class GlobalUserPreferences{
public static boolean playGifs; public static boolean playGifs;
public static boolean useCustomTabs; public static boolean useCustomTabs;
@@ -16,10 +13,6 @@ public class GlobalUserPreferences{
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
} }
private static SharedPreferences getPreReplyPrefs(){
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
}
public static void load(){ public static void load(){
SharedPreferences prefs=getPrefs(); SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true); playGifs=prefs.getBoolean("playGifs", true);
@@ -43,42 +36,9 @@ public class GlobalUserPreferences{
.apply(); .apply();
} }
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
return true;
if(account==null)
return false;
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
}
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
String key;
if(account==null){
key="opt_out_"+type;
}else{
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
key="opt_out_"+type+"_"+accountKey.toLowerCase();
}
getPreReplyPrefs().edit().putBoolean(key, true).apply();
}
public static void resetPreReplySheets(){
getPreReplyPrefs().edit().clear().apply();
}
public enum ThemePreference{ public enum ThemePreference{
AUTO, AUTO,
LIGHT, LIGHT,
DARK DARK
} }
public enum PreReplySheetType{
OLD_POST,
NON_MUTUAL
}
} }

View File

@@ -6,7 +6,6 @@ import android.app.Fragment;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.BadParcelableException;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@@ -37,15 +36,46 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity{ public class MainActivity extends FragmentStackActivity{
private static final String TAG="MainActivity";
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this); UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(savedInstanceState==null){ if(savedInstanceState==null){
restartHomeFragment(); if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new SplashFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.getBooleanExtra("fromNotification", false)){
String accountID=intent.getStringExtra("accountID");
try{
session=AccountSessionManager.getInstance().getAccount(accountID);
if(!intent.hasExtra("notification"))
args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}else{
maybeRequestNotificationsPermission();
}
}
} }
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){ if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
@@ -102,11 +132,11 @@ public class MainActivity extends FragmentStackActivity{
session=AccountSessionManager.get(accountID); session=AccountSessionManager.get(accountID);
if(session==null || !session.activated) if(session==null || !session.activated)
return; return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null); openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
} }
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){ public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, type, true, null, 0, 0) new GetSearchResults(q, null, true, null, 0, 0)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(SearchResults result){ public void onSuccess(SearchResults result){
@@ -170,47 +200,4 @@ public class MainActivity extends FragmentStackActivity{
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100); requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
} }
} }
public void restartHomeFragment(){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new SplashFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.getBooleanExtra("fromNotification", false)){
String accountID=intent.getStringExtra("accountID");
try{
session=AccountSessionManager.getInstance().getAccount(accountID);
if(!intent.hasExtra("notification"))
args.putString("tab", "notifications");
}catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
// Parcelables might not be compatible across app versions so this protects against possible crashes
// when a notification was received, then the app was updated, and then the user opened the notification
try{
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}catch(BadParcelableException x){
Log.w(TAG, x);
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}else{
maybeRequestNotificationsPermission();
}
}
}
} }

View File

@@ -118,8 +118,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values()) List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
.map(type->{ .map(type->{
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT); NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
channel.setLightColor(context.getColor(R.color.primary_700));
channel.enableLights(true);
channel.setGroup(accountID); channel.setGroup(accountID);
return channel; return channel;
}) })
@@ -149,7 +147,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setShowWhen(true) .setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL) .setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true) .setAutoCancel(true)
.setLights(context.getColor(R.color.primary_700), 500, 1000)
.setColor(context.getColor(R.color.primary_700)); .setColor(context.getColor(R.color.primary_700));
if(avatar!=null){ if(avatar!=null){
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar)); builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));

View File

@@ -9,31 +9,20 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -53,7 +42,6 @@ public class CacheController{
private final Runnable databaseCloseRunnable=this::closeDatabase; private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications; private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>(); private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private List<FollowList> lists;
private static final int POST_FLAG_GAP_AFTER=1; private static final int POST_FLAG_GAP_AFTER=1;
@@ -312,99 +300,6 @@ public class CacheController{
}, 0); }, 0);
} }
public void reloadLists(Callback<List<FollowList>> callback){
new GetLists()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<FollowList> result){
result.sort(Comparator.comparing(l->l.title));
lists=result;
if(callback!=null)
callback.onSuccess(result);
writeListsToFile();
}
@Override
public void onError(ErrorResponse error){
if(callback!=null)
callback.onError(error);
}
})
.exec(accountID);
}
private List<FollowList> loadListsFromFile(){
File file=getListsFile();
if(!file.exists())
return null;
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
}catch(Exception x){
Log.w(TAG, "failed to read lists from cache file", x);
return null;
}
}
private void writeListsToFile(){
databaseThread.postRunnable(()->{
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
MastodonAPIController.gson.toJson(lists, out);
}catch(IOException x){
Log.w(TAG, "failed to write lists to cache file", x);
}
}, 0);
}
public void getLists(Callback<List<FollowList>> callback){
if(lists!=null){
if(callback!=null)
callback.onSuccess(lists);
return;
}
databaseThread.postRunnable(()->{
List<FollowList> lists=loadListsFromFile();
if(lists!=null){
this.lists=lists;
if(callback!=null)
uiHandler.post(()->callback.onSuccess(lists));
return;
}
reloadLists(callback);
}, 0);
}
public File getListsFile(){
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
}
public void addList(FollowList list){
if(lists==null)
return;
lists.add(list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
}
public void deleteList(String id){
if(lists==null)
return;
lists.removeIf(l->l.id.equals(id));
writeListsToFile();
}
public void updateList(FollowList list){
if(lists==null)
return;
for(int i=0;i<lists.size();i++){
if(lists.get(i).id.equals(list.id)){
lists.set(i, list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
break;
}
}
}
private class DatabaseHelper extends SQLiteOpenHelper{ private class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){ public DatabaseHelper(){

View File

@@ -45,11 +45,7 @@ public class MastodonAPIController{
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter()) .registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
.create(); .create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController"); private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder() private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private AccountSession session; private AccountSession session;
@@ -96,15 +92,15 @@ public class MastodonAPIController{
} }
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+"Sending request: "+hreq); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
call.enqueue(new Callback(){ call.enqueue(new Callback(){
@Override @Override
public void onFailure(@NonNull Call call, @NonNull IOException e){ public void onFailure(@NonNull Call call, @NonNull IOException e){
if(req.canceled) if(call.isCanceled())
return; return;
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, logTag(session)+""+hreq+" failed", e); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
synchronized(req){ synchronized(req){
req.okhttpCall=null; req.okhttpCall=null;
} }
@@ -113,10 +109,10 @@ public class MastodonAPIController{
@Override @Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
if(req.canceled) if(call.isCanceled())
return; return;
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+hreq+" received response: "+response); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
synchronized(req){ synchronized(req){
req.okhttpCall=null; req.okhttpCall=null;
} }
@@ -127,7 +123,7 @@ public class MastodonAPIController{
try{ try{
if(BuildConfig.DEBUG){ if(BuildConfig.DEBUG){
JsonElement respJson=JsonParser.parseReader(reader); JsonElement respJson=JsonParser.parseReader(reader);
Log.d(TAG, logTag(session)+"response body: "+respJson); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null) if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType()); respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else if(req.respClass!=null) else if(req.respClass!=null)
@@ -144,7 +140,7 @@ public class MastodonAPIController{
} }
}catch(JsonIOException|JsonSyntaxException x){ }catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x); req.onError(x.getLocalizedMessage(), response.code(), x);
return; return;
} }
@@ -153,19 +149,19 @@ public class MastodonAPIController{
req.validateAndPostprocessResponse(respObj, response); req.validateAndPostprocessResponse(respObj, response);
}catch(IOException x){ }catch(IOException x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
req.onError(x.getLocalizedMessage(), response.code(), x); req.onError(x.getLocalizedMessage(), response.code(), x);
return; return;
} }
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
req.onSuccess(respObj); req.onSuccess(respObj);
}else{ }else{
try{ try{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject(); JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, logTag(session)+response+" received error: "+error); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
if(error.has("details")){ if(error.has("details")){
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null); MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>(); HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
@@ -200,7 +196,7 @@ public class MastodonAPIController{
}); });
}catch(Exception x){ }catch(Exception x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, logTag(session)+"error creating and sending http request", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
req.onError(x.getLocalizedMessage(), 0, x); req.onError(x.getLocalizedMessage(), 0, x);
} }
}, 0); }, 0);
@@ -213,8 +209,4 @@ public class MastodonAPIController{
public static OkHttpClient getHttpClient(){ public static OkHttpClient getHttpClient(){
return httpClient; return httpClient;
} }
private static String logTag(AccountSession session){
return "["+(session==null ? "no-auth" : session.getID())+"] ";
}
} }

View File

@@ -154,8 +154,6 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
} }
public RequestBody getRequestBody() throws IOException{ public RequestBody getRequestBody() throws IOException{
if(requestBody instanceof RequestBody rb)
return rb;
return requestBody==null ? null : new JsonObjectRequestBody(requestBody); return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
} }

View File

@@ -1,22 +0,0 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
public CheckInviteLink(String path){
super(HttpMethod.GET, path, Response.class);
addHeader("Accept", "application/json");
}
@Override
protected String getPathPrefix(){
return "";
}
public static class Response extends BaseModel{
@RequiredField
public String inviteCode;
}
}

View File

@@ -1,14 +0,0 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FollowList;
import java.util.List;
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
public GetAccountLists(String id){
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
}
}

View File

@@ -4,23 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{ public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){ public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
super(HttpMethod.POST, "/accounts", Token.class); super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode)); setRequestBody(new Body(username, email, password, locale, reason, timezone));
} }
private static class Body{ private static class Body{
public String username, email, password, locale, reason, timeZone, inviteCode; public String username, email, password, locale, reason, timeZone;
public boolean agreement=true; public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){ public Body(String username, String email, String password, String locale, String reason, String timeZone){
this.username=username; this.username=username;
this.email=email; this.email=email;
this.password=password; this.password=password;
this.locale=locale; this.locale=locale;
this.reason=reason; this.reason=reason;
this.timeZone=timeZone; this.timeZone=timeZone;
this.inviteCode=inviteCode;
} }
} }
} }

View File

@@ -1,23 +0,0 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
import java.util.List;
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
addQueryParameter("q", q);
if(limit>0)
addQueryParameter("limit", limit+"");
if(offset>0)
addQueryParameter("offset", offset+"");
if(resolve)
addQueryParameter("resolve", "true");
if(following)
addQueryParameter("following", "true");
}
}

View File

@@ -22,7 +22,6 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
private Uri avatar, cover; private Uri avatar, cover;
private File avatarFile, coverFile; private File avatarFile, coverFile;
private List<AccountField> fields; private List<AccountField> fields;
private Boolean discoverable, indexable;
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){ public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class); super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
@@ -42,12 +41,6 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
this.fields=fields; this.fields=fields;
} }
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
this.discoverable=discoverable;
this.indexable=indexable;
return this;
}
@Override @Override
public RequestBody getRequestBody() throws IOException{ public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder bldr=new MultipartBody.Builder() MultipartBody.Builder bldr=new MultipartBody.Builder()
@@ -65,21 +58,15 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
}else if(coverFile!=null){ }else if(coverFile!=null){
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null)); bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
} }
if(fields!=null){ if(fields.isEmpty()){
if(fields.isEmpty()){ bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", ""); }else{
}else{ int i=0;
int i=0; for(AccountField field:fields){
for(AccountField field:fields){ bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value); i++;
i++;
}
} }
} }
if(discoverable!=null)
bldr.addFormDataPart("discoverable", discoverable.toString());
if(indexable!=null)
bldr.addFormDataPart("indexable", indexable.toString());
return bldr.build(); return bldr.build();
} }

View File

@@ -2,9 +2,6 @@ package org.joinmastodon.android.api.requests.filters;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import androidx.annotation.Keep;
@Keep
class KeywordAttribute{ class KeywordAttribute{
public String id; public String id;
@SerializedName("_destroy") @SerializedName("_destroy")

View File

@@ -1,19 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import okhttp3.FormBody;
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
public AddAccountsToList(String listID, Collection<String> accountIDs){
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
for(String id:accountIDs){
builder.add("account_ids[]", id);
}
setRequestBody(builder.build());
}
}

View File

@@ -1,23 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FollowList;
public class CreateList extends MastodonAPIRequest<FollowList>{
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
super(HttpMethod.POST, "/lists", FollowList.class);
setRequestBody(new Request(title, repliesPolicy, exclusive));
}
private static class Request{
public String title;
public FollowList.RepliesPolicy repliesPolicy;
public boolean exclusive;
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
this.title=title;
this.repliesPolicy=repliesPolicy;
this.exclusive=exclusive;
}
}
}

View File

@@ -1,9 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class DeleteList extends ResultlessMastodonAPIRequest{
public DeleteList(String id){
super(HttpMethod.DELETE, "/lists/"+id);
}
}

View File

@@ -1,17 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
public class GetListAccounts extends HeaderPaginationRequest<Account>{
public GetListAccounts(String listID, String maxID, int limit){
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
addQueryParameter("limit", String.valueOf(limit));
}
}

View File

@@ -1,14 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FollowList;
import java.util.List;
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
public GetLists(){
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
}
}

View File

@@ -1,19 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import okhttp3.FormBody;
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
for(String id:accountIDs){
builder.add("account_ids[]", id);
}
setRequestBody(builder.build());
}
}

View File

@@ -1,23 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FollowList;
public class UpdateList extends MastodonAPIRequest<FollowList>{
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
setRequestBody(new Request(title, repliesPolicy, exclusive));
}
private static class Request{
public String title;
public FollowList.RepliesPolicy repliesPolicy;
public boolean exclusive;
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
this.title=title;
this.repliesPolicy=repliesPolicy;
this.exclusive=exclusive;
}
}
}

View File

@@ -26,11 +26,6 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
s.visibility=StatusPrivacy.PUBLIC; s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList(); s.mentions=Collections.emptyList();
s.tags=Collections.emptyList(); s.tags=Collections.emptyList();
if(s.poll!=null){
s.poll.id="fakeID"+i;
s.poll.emojis=Collections.emptyList();
s.poll.ownVotes=Collections.emptyList();
}
i++; i++;
} }
super.validateAndPostprocessResponse(respObj, httpResponse); super.validateAndPostprocessResponse(respObj, httpResponse);

View File

@@ -1,16 +0,0 @@
package org.joinmastodon.android.api.requests.tags;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Hashtag;
public class GetFollowedTags extends HeaderPaginationRequest<Hashtag>{
public GetFollowedTags(String maxID, int limit){
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -1,22 +0,0 @@
package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetListTimeline extends MastodonAPIRequest<List<Status>>{
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID){
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
}
}

View File

@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID){ public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){}); super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
if(local) if(local)
addQueryParameter("local", "true"); addQueryParameter("local", "true");
@@ -18,10 +18,6 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("remote", "true"); addQueryParameter("remote", "true");
if(!TextUtils.isEmpty(maxID)) if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
if(!TextUtils.isEmpty(minID))
addQueryParameter("min_id", minID);
if(!TextUtils.isEmpty(sinceID))
addQueryParameter("since_id", sinceID);
if(limit>0) if(limit>0)
addQueryParameter("limit", limit+""); addQueryParameter("limit", limit+"");
} }

View File

@@ -24,7 +24,6 @@ import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult; import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
@@ -33,7 +32,6 @@ import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator; import org.joinmastodon.android.utils.ObjectIdComparator;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -68,7 +66,6 @@ public class AccountSession{
private transient SharedPreferences prefs; private transient SharedPreferences prefs;
private transient boolean preferencesNeedSaving; private transient boolean preferencesNeedSaving;
private transient AccountLocalPreferences localPreferences; private transient AccountLocalPreferences localPreferences;
private transient List<FollowList> lists;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){ AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token; this.token=token;
@@ -268,12 +265,4 @@ public class AccountSession{
public void updateAccountInfo(){ public void updateAccountInfo(){
AccountSessionManager.getInstance().updateSessionLocalInfo(this); AccountSessionManager.getInstance().updateSessionLocalInfo(this);
} }
public boolean isNotificationsMentionsOnly(){
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
}
public void setNotificationsMentionsOnly(boolean mentionsOnly){
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
}
} }

View File

@@ -175,17 +175,12 @@ public class AccountSessionManager{
public void removeAccount(String id){ public void removeAccount(String id){
AccountSession session=getAccount(id); AccountSession session=getAccount(id);
session.getCacheController().closeDatabase(); session.getCacheController().closeDatabase();
session.getCacheController().getListsFile().delete();
MastodonApp.context.deleteDatabase(id+".db"); MastodonApp.context.deleteDatabase(id+".db");
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit(); MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
MastodonApp.context.deleteSharedPreferences(id); MastodonApp.context.deleteSharedPreferences(id);
}else{ }else{
String dataDir=MastodonApp.context.getApplicationInfo().dataDir; new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
if(dataDir!=null){
File prefsDir=new File(dataDir, "shared_prefs");
new File(prefsDir, id+".xml").delete();
}
} }
sessions.remove(id); sessions.remove(id);
if(lastActiveAccountID.equals(id)){ if(lastActiveAccountID.equals(id)){

View File

@@ -1,15 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Account;
public class AccountAddedToListEvent{
public final String accountID;
public final String listID;
public final Account account;
public AccountAddedToListEvent(String accountID, String listID, Account account){
this.accountID=accountID;
this.listID=listID;
this.account=account;
}
}

View File

@@ -1,13 +0,0 @@
package org.joinmastodon.android.events;
public class AccountRemovedFromListEvent{
public final String accountID;
public final String listID;
public final String targetAccountID;
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
this.accountID=accountID;
this.listID=listID;
this.targetAccountID=targetAccountID;
}
}

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.events;
public class FinishListCreationFragmentEvent{
public final String accountID;
public final String listID;
public FinishListCreationFragmentEvent(String accountID, String listID){
this.accountID=accountID;
this.listID=listID;
}
}

View File

@@ -1,13 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.FollowList;
public class ListCreatedEvent{
public final String accountID;
public final FollowList list;
public ListCreatedEvent(String accountID, FollowList list){
this.accountID=accountID;
this.list=list;
}
}

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.events;
public class ListDeletedEvent{
public final String accountID;
public final String listID;
public ListDeletedEvent(String accountID, String listID){
this.accountID=accountID;
this.listID=listID;
}
}

View File

@@ -1,13 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.FollowList;
public class ListUpdatedEvent{
public final String accountID;
public final FollowList list;
public ListUpdatedEvent(String accountID, FollowList list){
this.accountID=accountID;
this.list=list;
}
}

View File

@@ -1,114 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
private Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.add_user_to_list_title);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
loadData();
}
@Override
protected void doLoadData(int offset, int count){
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> allLists){
if(getActivity()==null)
return;
loadAccountLists(allLists);
}
});
}
private void loadAccountLists(final List<FollowList> allLists){
currentRequest=new GetAccountLists(account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> result){
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
onDataLoaded(allLists.stream()
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
.collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected int indexOfItemsAdapter(){
return 1;
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView topText=new TextView(getActivity());
topText.setTextAppearance(R.style.m3_body_medium);
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onItemClick(CheckableListItem<FollowList> item){
boolean add=!item.checked;
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
req.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
item.checked=add;
rebindItem(item);
if(add){
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
}else{
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
}

View File

@@ -1,176 +0,0 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
protected FollowList followList;
protected AvatarPileListItem<Void> membersItem;
protected CheckableListItem<Void> exclusiveItem;
protected FloatingHintEditTextLayout titleEditLayout;
protected EditText titleEdit;
protected Spinner showRepliesSpinner;
private APIRequest<?> getMembersRequest;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
List<ListItem<Void>> items=new ArrayList<>();
if(followList!=null){
items.add(membersItem);
}
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
items.add(exclusiveItem);
onDataLoaded(items);
}
@Override
public void onDestroy(){
super.onDestroy();
if(getMembersRequest!=null)
getMembersRequest.cancel();
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
LinearLayout topView=new LinearLayout(getActivity());
topView.setOrientation(LinearLayout.VERTICAL);
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
titleEdit=titleEditLayout.findViewById(R.id.edit);
titleEdit.setHint(R.string.list_name);
titleEditLayout.updateHint();
if(followList!=null)
titleEdit.setText(followList.title);
topView.addView(titleEditLayout);
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
showRepliesLayout.setHint(R.string.list_show_replies_to);
topView.addView(showRepliesLayout);
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
getString(R.string.list_replies_no_one),
getString(R.string.list_replies_members),
getString(R.string.list_replies_anyone)
));
showRepliesSpinner.setAdapter(spinnerAdapter);
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
case FOLLOWED -> 2;
case LIST -> 1;
case NONE -> 0;
});
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected int indexOfItemsAdapter(){
return 1;
}
protected void doDeleteList(){
new DeleteList(followList.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
E.post(new ListDeletedEvent(accountID, followList.id));
Nav.finish(BaseEditListFragment.this);
}
@Override
public void onError(ErrorResponse error){
Activity activity=getActivity();
if(activity==null)
return;
error.showToast(activity);
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void onMembersClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
Nav.go(getActivity(), ListMembersFragment.class, args);
}
protected void loadMembers(){
getMembersRequest=new GetListAccounts(followList.id, null, 3)
.setCallback(new Callback<>(){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
getMembersRequest=null;
membersItem.avatars=new ArrayList<>();
for(int i=0;i<Math.min(3, result.size());i++){
Account acc=result.get(i);
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
}
rebindItem(membersItem);
imgLoader.updateImages();
}
@Override
public void onError(ErrorResponse error){
getMembersRequest=null;
}
})
.exec(accountID);
}
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
return switch(showRepliesSpinner.getSelectedItemPosition()){
case 0 -> FollowList.RepliesPolicy.NONE;
case 1 -> FollowList.RepliesPolicy.LIST;
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
};
}
}

View File

@@ -14,12 +14,10 @@ import android.view.WindowInsets;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote; import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus; import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
@@ -29,9 +27,8 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation; import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
@@ -46,8 +43,6 @@ import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.TypedObjectPool; import org.joinmastodon.android.utils.TypedObjectPool;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@@ -111,7 +106,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(T s:items){ for(T s:items){
displayItems.addAll(buildDisplayItems(s)); displayItems.addAll(buildDisplayItems(s));
} }
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
} }
@Override @Override
@@ -133,7 +127,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
if(notify) if(notify)
adapter.notifyItemRangeInserted(0, offset); adapter.notifyItemRangeInserted(0, offset);
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
} }
protected String getMaxID(){ protected String getMaxID(){
@@ -181,7 +174,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override @Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){ public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus(); final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){ currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder; private MediaAttachmentViewController transitioningHolder;
@Override @Override
@@ -247,7 +240,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override @Override
public void photoViewerDismissed(){ public void photoViewerDismissed(){
currentPhotoViewer=null; currentPhotoViewer=null;
gridHolder.itemView.setHasTransientState(false);
} }
@Override @Override
@@ -259,7 +251,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return gridHolder.getViewController(index); return gridHolder.getViewController(index);
} }
}); });
gridHolder.itemView.setHasTransientState(true);
} }
@Override @Override
@@ -366,7 +357,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1); List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
int prevSize=pollItems.size(); int prevSize=pollItems.size();
pollItems.clear(); pollItems.clear();
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems); StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems);
if(prevSize!=pollItems.size()){ if(prevSize!=pollItems.size()){
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize); adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size()); adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
@@ -464,9 +455,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
protected void loadRelationships(Set<String> ids){ protected void loadRelationships(Set<String> ids){
if(ids.isEmpty())
return;
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
if(ids.isEmpty()) if(ids.isEmpty())
return; return;
// TODO somehow manage these and cancel outstanding requests on refresh // TODO somehow manage these and cancel outstanding requests on refresh
@@ -598,7 +586,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return; return;
status.translation=result; status.translation=result;
status.translationState=Status.TranslationState.SHOWN; status.translationState=Status.TranslationState.SHOWN;
updateTranslation(itemID); TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}
} }
@Override @Override
@@ -606,7 +598,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(getActivity()==null) if(getActivity()==null)
return; return;
status.translationState=Status.TranslationState.HIDDEN; status.translationState=Status.TranslationState.HIDDEN;
updateTranslation(itemID); TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
}
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.translation_failed) .setMessage(R.string.translation_failed)
@@ -618,31 +613,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
} }
} }
updateTranslation(itemID);
}
private void updateTranslation(String itemID) {
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){ if(text!=null){
text.updateTranslation(true); text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition()); imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
} }
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null){
spoiler.rebind();
}
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if (media!=null) {
media.rebind();
}
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
item.rebind();
}
}
} }
public void rebuildAllDisplayItems(){ public void rebuildAllDisplayItems(){
@@ -653,26 +628,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
} }
public void maybeShowPreReplySheet(Status status, Runnable proceed){
Relationship rel=getRelationship(status.account.id);
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
new NonMutualPreReplySheet(getActivity(), notAgain->{
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
proceed.run();
}, status.account, accountID).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
new OldPostPreReplySheet(getActivity(), notAgain->{
if(notAgain)
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
proceed.run();
}, status).show();
}else{
proceed.run();
}
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){} protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -745,7 +700,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
// Do not draw dividers between hashtag and/or account rows // Do not draw dividers between hashtag and/or account rows
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder)) if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
return false; return false;
return !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP; return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
} }
return false; return false;
} }

View File

@@ -1,8 +1,5 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.ClipData; import android.content.ClipData;
@@ -22,14 +19,17 @@ import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.SoundEffectConstants;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider; import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@@ -49,6 +49,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.statuses.CreateStatus; import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.EditStatus; import org.joinmastodon.android.api.requests.statuses.EditStatus;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
@@ -56,7 +57,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment; import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
@@ -93,14 +94,12 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
private static final int MEDIA_RESULT=717; private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363; public static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -341,7 +340,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onLaunchAccountSearch(){ public void onLaunchAccountSearch(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this); Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
} }
}); });
View autocompleteView=autocompleteViewController.getView(); View autocompleteView=autocompleteViewController.getView();
@@ -1017,26 +1016,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"}; return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
} }
private String sanitizeMediaDescription(String description){
if(description == null){
return null;
}
// The Gboard android keyboard attaches this text whenever the user
// pastes something from the keyboard's suggestion bar.
// Due to different end user locales, the exact text may vary, but at
// least in version 13.4.08, all of the translations contained the
// string "Gboard".
if (description.contains("Gboard")){
return null;
}
return description;
}
@Override @Override
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){ public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
description = sanitizeMediaDescription(description);
return mediaViewController.addMediaAttachment(uri, description); return mediaViewController.addMediaAttachment(uri, description);
} }
@@ -1077,8 +1058,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Editable e=mainEditText.getText(); Editable e=mainEditText.getText();
int start=e.getSpanStart(currentAutocompleteSpan); int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan); int end=e.getSpanEnd(currentAutocompleteSpan);
if(start==-1 || end==-1)
return;
e.replace(start, end, text+" "); e.replace(start, end, text+" ");
finishAutocomplete(); finishAutocomplete();
InputConnection conn=mainEditText.getCurrentInputConnection(); InputConnection conn=mainEditText.getCurrentInputConnection();
@@ -1131,35 +1110,4 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){ private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){
postLang=language; postLang=language;
} }
@Override
public Animator onCreateEnterTransition(View prev, View container){
AnimatorSet anim=new AnimatorSet();
if(getArguments().getBoolean("fromThreadFragment")){
anim.playTogether(
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
);
}else{
anim.playTogether(
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
);
}
anim.setDuration(300);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
return anim;
}
@Override
public Animator onCreateExitTransition(View prev, View container){
AnimatorSet anim=new AnimatorSet();
anim.playTogether(
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
);
anim.setDuration(200);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
return anim;
}
} }

View File

@@ -7,7 +7,10 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log; import android.util.Log;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -128,9 +131,20 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){ if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper) new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text) .setTitle(R.string.what_is_alt_text)
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help)) .setMessage(msg)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show(); .show();
} }
@@ -167,7 +181,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
fakeAttachment.meta.width=width; fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height; fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){ photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
@Override @Override
public void setPhotoViewVisibility(int index, boolean visible){ public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f); image.setAlpha(visible ? 1f : 0f);

View File

@@ -1,323 +0,0 @@
package org.joinmastodon.android.fragments;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
import org.parceler.Parcels;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.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{
private FollowList followList;
private Button nextButton;
private View buttonBar;
private FragmentRootLinearLayout rootView;
private FrameLayout searchFragmentContainer;
private FrameLayout fragmentContentWrap;
private AddNewListMembersFragment searchFragment;
private WindowInsets lastInsets;
private boolean dismissingSearchFragment;
private HashSet<String> accountIDsInList=new HashSet<>();
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_list_members);
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
setLayout(R.layout.fragment_login);
setEmptyText(R.string.list_no_members);
setHasOptionsMenu(true);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
loadData();
}else{
onDataLoaded(List.of());
}
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetListAccounts(followList.id, null, 0)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
for(Account acc:result)
accountIDsInList.add(acc.id);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
}
})
.exec(accountID);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=super.onCreateView(inflater, container, savedInstanceState);
FrameLayout wrapper=new FrameLayout(getActivity());
wrapper.addView(view);
rootView=(FragmentRootLinearLayout) view;
fragmentContentWrap=wrapper;
return wrapper;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setText(R.string.done);
buttonBar=view.findViewById(R.id.button_bar);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
lastInsets=insets;
if(searchFragment!=null)
searchFragment.onApplyWindowInsets(insets);
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
rootView.dispatchApplyWindowInsets(insets);
}
@Override
protected List<View> getViewsForElevationEffect(){
return List.of(getToolbar(), buttonBar);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
MenuItem item=menu.add(R.string.add_list_member);
item.setIcon(R.drawable.ic_add_24px);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(searchFragmentContainer!=null)
return true;
searchFragmentContainer=new FrameLayout(getActivity());
searchFragmentContainer.setId(R.id.search_fragment);
fragmentContentWrap.addView(searchFragmentContainer);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("_can_go_back", true);
searchFragment=new AddNewListMembersFragment(this);
searchFragment.setArguments(args);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
if(lastInsets!=null)
searchFragment.onApplyWindowInsets(lastInsets);
searchFragmentContainer.setTranslationX(V.dp(100));
searchFragmentContainer.setAlpha(0f);
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
return true;
}
@Override
protected void initializeEmptyView(View contentView){
ViewStub emptyStub=contentView.findViewById(R.id.empty);
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
super.initializeEmptyView(contentView);
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
emptySecondary.setText(R.string.list_find_users);
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
}
@Override
protected void setStatusBarColor(int color){
rootView.setStatusBarColor(color);
}
@Override
protected void setNavigationBarColor(int color){
rootView.setNavigationBarColor(color);
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
fragmentContentWrap.removeView(searchFragmentContainer);
searchFragmentContainer=null;
searchFragment=null;
dismissingSearchFragment=false;
}).start();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
private void onNextClick(View v){
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
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);
}
@Override
public void addAccountToList(AccountViewModel account, Runnable onDone){
new AddAccountsToList(followList.id, Set.of(account.account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
accountIDsInList.add(account.account.id);
if(onDone!=null)
onDone.run();
int i=0;
for(AccountViewModel acc:data){
if(acc.account.id.equals(account.account.id)){
list.getAdapter().notifyItemChanged(i);
return;
}
i++;
}
int pos=data.size();
data.add(account);
list.getAdapter().notifyItemInserted(pos);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
}
@Override
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
accountIDsInList.remove(account.account.id);
if(onDone!=null)
onDone.run();
int i=0;
for(AccountViewModel acc:data){
if(acc.account.id.equals(account.account.id)){
list.getAdapter().notifyItemChanged(i);
return;
}
i++;
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
holder.setOnLongClickListener(vh->false);
Button button=holder.getButton();
button.setPadding(V.dp(24), 0, V.dp(24), 0);
button.setMinimumWidth(0);
button.setMinWidth(0);
button.setOnClickListener(v->{
holder.setActionProgressVisible(true);
holder.itemView.setHasTransientState(true);
Runnable onDone=()->{
holder.setActionProgressVisible(false);
holder.itemView.setHasTransientState(false);
};
AccountViewModel account=holder.getItem();
if(isAccountInList(account)){
removeAccountAccountFromList(account, onDone);
}else{
addAccountToList(account, onDone);
}
});
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
Button button=holder.getButton();
int textRes, styleRes;
if(isAccountInList(holder.getItem())){
textRes=R.string.remove;
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
}else{
textRes=R.string.add;
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
button.setText(textRes);
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
button.setTextColor(ta.getColorStateList(0));
ta.recycle();
}
@Override
protected void loadRelationships(List<AccountViewModel> accounts){
// no-op
}
}

View File

@@ -1,149 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
import org.joinmastodon.android.events.ListCreatedEvent;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class CreateListFragment extends BaseEditListFragment{
private Button nextButton;
private View buttonBar;
private FollowList followList;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.create_list);
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
setLayout(R.layout.fragment_login);
if(savedInstanceState!=null)
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24;
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setText(R.string.create);
buttonBar=view.findViewById(R.id.button_bar);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected List<View> getViewsForElevationEffect(){
return List.of(getToolbar(), buttonBar);
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putParcelable("list", Parcels.wrap(followList));
}
private void onNextClick(View v){
String title=titleEdit.getText().toString().trim();
if(TextUtils.isEmpty(title)){
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
return;
}
if(followList==null){
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
followList=result;
proceed(false);
E.post(new ListCreatedEvent(accountID, result));
AccountSessionManager.get(accountID).getCacheController().addList(result);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
followList=result;
proceed(true);
E.post(new ListUpdatedEvent(accountID, result));
AccountSessionManager.get(accountID).getCacheController().updateList(result);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}else{
proceed(true);
}
}
private void proceed(boolean needLoadMembers){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("needLoadMembers", needLoadMembers);
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
@Subscribe
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
Nav.finish(this);
}
}
}

View File

@@ -1,67 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class EditListFragment extends BaseEditListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.edit_list);
loadMembers();
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
menu.add(R.string.delete_list);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.delete_list)
.setMessage(getString(R.string.delete_list_confirm, followList.title))
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
.setNegativeButton(R.string.cancel, null)
.show();
return true;
}
@Override
public void onDestroy(){
super.onDestroy();
String newTitle=titleEdit.getText().toString();
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
boolean newExclusive=exclusiveItem.checked;
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
.setCallback(new Callback<>(){
@Override
public void onSuccess(FollowList result){
AccountSessionManager.get(accountID).getCacheController().updateList(result);
E.post(new ListUpdatedEvent(accountID, result));
}
@Override
public void onError(ErrorResponse error){
// TODO handle errors somehow
}
})
.exec(accountID);
}
}
}

View File

@@ -18,8 +18,6 @@ import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.tags.GetTag; import org.joinmastodon.android.api.requests.tags.GetTag;
import org.joinmastodon.android.api.requests.tags.SetTagFollowed; import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline; import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.SpacerSpan; import org.joinmastodon.android.ui.text.SpacerSpan;
@@ -49,7 +47,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
private MenuItem followMenuItem; private MenuItem followMenuItem;
private boolean followRequestRunning; private boolean followRequestRunning;
private boolean toolbarContentVisible; private boolean toolbarContentVisible;
private String maxID;
public HashtagTimelineFragment(){ public HashtagTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab); setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -70,13 +67,10 @@ public class HashtagTimelineFragment extends StatusListFragment{
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : maxID, null, count) currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : getMaxID(), null, count)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -182,7 +176,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
} }
private void updateHeader(){ private void updateHeader(){
if(hashtag==null || getActivity()==null) if(hashtag==null)
return; return;
if(hashtag.history!=null && !hashtag.history.isEmpty()){ if(hashtag.history!=null && !hashtag.history.isEmpty()){

View File

@@ -30,7 +30,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.ui.views.TabBar;
@@ -150,7 +150,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}); });
} }
} }
tabBar.selectTab(currentTab);
return content; return content;
} }

View File

@@ -5,50 +5,40 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.app.Activity; import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity; import android.view.Gravity;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AnimationUtils;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.markers.SaveMarkers; import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment; import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers; import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@@ -62,143 +52,44 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{ public class HomeTimelineFragment extends StatusListFragment{
private ImageButton fab; private ImageButton fab;
private LinearLayout listsDropdown; private ImageView toolbarLogo;
private FixedAspectRatioImageView listsDropdownArrow; private Button toolbarShowNewPostsBtn;
private TextView listsDropdownText;
private Button newPostsBtn;
private View newPostsBtnWrap;
private boolean newPostsBtnShown; private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim; private AnimatorSet currentNewPostsAnim;
private ToolbarDropdownMenuController dropdownController;
private HomeTimelineMenuController dropdownMainMenuController;
private List<FollowList> lists=List.of();
private ListMode listMode=ListMode.FOLLOWING;
private FollowList currentList;
private MergeRecyclerAdapter mergeAdapter;
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private String maxID; private String maxID;
private String lastSavedMarkerID; private String lastSavedMarkerID;
public HomeTimelineFragment(){ public HomeTimelineFragment(){
setListLayoutId(R.layout.fragment_timeline); setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
} }
@Override @Override
public void onAttach(Activity activity){ public void onAttach(Activity activity){
super.onAttach(activity); super.onAttach(activity);
dropdownController=new ToolbarDropdownMenuController(this);
dropdownMainMenuController=new HomeTimelineMenuController(dropdownController, new HomeTimelineMenuController.Callback(){
@Override
public void onFollowingSelected(){
if(listMode==ListMode.FOLLOWING)
return;
listMode=ListMode.FOLLOWING;
reload();
}
@Override
public void onLocalSelected(){
if(listMode==ListMode.LOCAL)
return;
listMode=ListMode.LOCAL;
reload();
}
@Override
public List<FollowList> getLists(){
return lists;
}
@Override
public void onListSelected(FollowList list){
if(listMode==ListMode.LIST && currentList==list)
return;
listMode=ListMode.LIST;
currentList=list;
reload();
}
});
setHasOptionsMenu(true); setHasOptionsMenu(true);
loadData(); loadData();
AccountSessionManager.get(accountID).getCacheController().getLists(new Callback<>(){
@Override
public void onSuccess(List<FollowList> result){
lists=result;
}
@Override
public void onError(ErrorResponse error){}
});
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
switch(listMode){ AccountSessionManager.getInstance()
case FOLLOWING -> { .getAccount(accountID).getCacheController()
AccountSessionManager.getInstance() .getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
.getAccount(accountID).getCacheController() @Override
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){ public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
@Override if(getActivity()==null)
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){ return;
if(getActivity()==null || listMode!=ListMode.FOLLOWING) onDataLoaded(result.items, !result.items.isEmpty());
return; maxID=result.maxID;
if(refreshing) if(result.isFromCache())
list.scrollToPosition(0); loadNewPosts();
onDataLoaded(result.items, !result.items.isEmpty()); }
maxID=result.maxID; });
if(result.isFromCache())
loadNewPosts();
}
@Override
public void onError(ErrorResponse error){
if(listMode!=ListMode.FOLLOWING)
return;
super.onError(error);
}
});
}
case LOCAL -> {
currentRequest=new GetPublicTimeline(true, false, offset>0 ? maxID : null, null, count, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
case LIST -> {
currentRequest=new GetListTimeline(currentList.id, offset>0 ? maxID : null, null, count, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
}
} }
@Override @Override
@@ -206,19 +97,6 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab); fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick); fab.setOnClickListener(this::onFabClick);
newPostsBtn=view.findViewById(R.id.new_posts_btn);
newPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
newPostsBtnWrap=view.findViewById(R.id.new_posts_btn_wrap);
if(newPostsBtnShown){
newPostsBtnWrap.setVisibility(View.VISIBLE);
}else{
newPostsBtnWrap.setVisibility(View.GONE);
newPostsBtnWrap.setScaleX(0.9f);
newPostsBtnWrap.setScaleY(0.9f);
newPostsBtnWrap.setAlpha(0f);
newPostsBtnWrap.setTranslationY(V.dp(-56));
}
updateToolbarLogo(); updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){ list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override @Override
@@ -238,26 +116,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.home, menu); inflater.inflate(R.menu.home, menu);
menu.findItem(R.id.edit_list).setVisible(listMode==ListMode.LIST);
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.UpdateState.NO_UPDATE;
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
if(updater!=null)
state=updater.getState();
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
int id=item.getItemId(); Nav.go(getActivity(), SettingsMainFragment.class, args);
if(id==R.id.settings){
Nav.go(getActivity(), SettingsMainFragment.class, args);
}else if(id==R.id.edit_list){
args.putParcelable("list", Parcels.wrap(currentList));
Nav.go(getActivity(), EditListFragment.class, args);
}
return true; return true;
} }
@@ -282,7 +147,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
@Override @Override
protected void onHidden(){ protected void onHidden(){
super.onHidden(); super.onHidden();
if(!data.isEmpty() && listMode==ListMode.FOLLOWING){ if(!data.isEmpty()){
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID; String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
if(!topPostID.equals(lastSavedMarkerID)){ if(!topPostID.equals(lastSavedMarkerID)){
lastSavedMarkerID=topPostID; lastSavedMarkerID=topPostID;
@@ -318,8 +183,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
// we'll get the currently topmost post as last in the response. This way we know there's no gap // we'll get the currently topmost post as last in the response. This way we know there's no gap
// between the existing and newly loaded parts of the timeline. // between the existing and newly loaded parts of the timeline.
String sinceID=data.size()>1 ? data.get(1).id : "1"; String sinceID=data.size()>1 ? data.get(1).id : "1";
boolean needCache=listMode==ListMode.FOLLOWING; currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
loadAdditionalPosts(null, null, 20, sinceID, new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
currentRequest=null; currentRequest=null;
@@ -334,13 +199,11 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
result.get(result.size()-1).hasGapAfter=true; result.get(result.size()-1).hasGapAfter=true;
toAdd=result; toAdd=result;
} }
if(needCache) AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
if(!toAdd.isEmpty()){ if(!toAdd.isEmpty()){
prependItems(toAdd, true); prependItems(toAdd, true);
showNewPostsButton(); showNewPostsButton();
if(needCache) AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
} }
} }
@@ -349,7 +212,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
currentRequest=null; currentRequest=null;
dataLoading=false; dataLoading=false;
} }
}); })
.exec(accountID);
} }
@Override @Override
@@ -361,11 +225,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
V.setVisibilityAnimated(item.text, View.GONE); V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem(); GapStatusDisplayItem gap=item.getItem();
dataLoading=true; dataLoading=true;
boolean needCache=listMode==ListMode.FOLLOWING; currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
currentRequest=null; currentRequest=null;
dataLoading=false; dataLoading=false;
if(getActivity()==null) if(getActivity()==null)
@@ -379,8 +242,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
Status gapStatus=getStatusByID(gap.parentID); Status gapStatus=getStatusByID(gap.parentID);
if(gapStatus!=null){ if(gapStatus!=null){
gapStatus.hasGapAfter=false; gapStatus.hasGapAfter=false;
if(needCache) AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
} }
}else{ }else{
Set<String> idsBelowGap=new HashSet<>(); Set<String> idsBelowGap=new HashSet<>();
@@ -392,8 +254,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}else if(s.id.equals(gap.parentID)){ }else if(s.id.equals(gap.parentID)){
belowGap=true; belowGap=true;
s.hasGapAfter=false; s.hasGapAfter=false;
if(needCache) AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
}else{ }else{
gapPostIndex++; gapPostIndex++;
} }
@@ -409,8 +270,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}else{ }else{
result=result.subList(0, endIndex); result=result.subList(0, endIndex);
} }
if(needCache) AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear(); targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1); List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
@@ -427,8 +287,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos); adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1); adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
} }
if(needCache) AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
} }
} }
@@ -445,17 +304,9 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
adapter.notifyItemChanged(gapPos); adapter.notifyItemChanged(gapPos);
} }
} }
}); })
} .exec(accountID);
private void loadAdditionalPosts(String maxID, String minID, int limit, String sinceID, Callback<List<Status>> callback){
MastodonAPIRequest<List<Status>> req=switch(listMode){
case FOLLOWING -> new GetHomeTimeline(maxID, minID, limit, sinceID);
case LOCAL -> new GetPublicTimeline(true, false, maxID, minID, limit, sinceID);
case LIST -> new GetListTimeline(currentList.id, maxID, minID, limit, sinceID);
};
currentRequest=req;
req.setCallback(callback).exec(accountID);
} }
@Override @Override
@@ -469,41 +320,42 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
} }
private void updateToolbarLogo(){ private void updateToolbarLogo(){
listsDropdown=new LinearLayout(getActivity()); toolbarLogo=new ImageView(getActivity());
listsDropdown.setOnClickListener(this::onListsDropdownClick); toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
listsDropdown.setBackgroundResource(R.drawable.bg_button_m3_text); toolbarLogo.setImageResource(R.drawable.logo);
listsDropdown.setAccessibilityDelegate(new View.AccessibilityDelegate(){ toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){ toolbarShowNewPostsBtn=new Button(getActivity());
super.onInitializeAccessibilityNodeInfo(host, info); toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
info.setClassName("android.widget.Spinner"); toolbarShowNewPostsBtn.setTextColor(0xffffffff);
} toolbarShowNewPostsBtn.setStateListAnimator(null);
}); toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
listsDropdownArrow=new FixedAspectRatioImageView(getActivity()); toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
listsDropdownArrow.setUseHeight(true); toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
listsDropdownArrow.setImageResource(R.drawable.ic_arrow_drop_down_24px); toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
listsDropdownArrow.setScaleType(ImageView.ScaleType.CENTER); toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
listsDropdownArrow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
listsDropdown.addView(listsDropdownArrow, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
listsDropdownText=new TextView(getActivity()); toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
listsDropdownText.setTextAppearance(R.style.action_bar_title);
listsDropdownText.setSingleLine(); if(newPostsBtnShown){
listsDropdownText.setEllipsize(TextUtils.TruncateAt.END); toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); toolbarLogo.setVisibility(View.INVISIBLE);
listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0); toolbarLogo.setAlpha(0f);
listsDropdownText.setText(getCurrentListTitle()); }else{
listsDropdownArrow.setImageTintList(listsDropdownText.getTextColors()); toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
listsDropdown.setBackgroundTintList(listsDropdownText.getTextColors()); toolbarShowNewPostsBtn.setAlpha(0f);
listsDropdown.addView(listsDropdownText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); toolbarShowNewPostsBtn.setScaleX(.8f);
toolbarShowNewPostsBtn.setScaleY(.8f);
toolbarLogo.setVisibility(View.VISIBLE);
}
FrameLayout logoWrap=new FrameLayout(getActivity()); FrameLayout logoWrap=new FrameLayout(getActivity());
FrameLayout.LayoutParams ddlp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START); logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
ddlp.topMargin=ddlp.bottomMargin=V.dp(8); logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
logoWrap.addView(listsDropdown, ddlp);
Toolbar toolbar=getToolbar(); Toolbar toolbar=getToolbar();
toolbar.addView(logoWrap, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
toolbar.setContentInsetsRelative(V.dp(16), 0);
} }
private void showNewPostsButton(){ private void showNewPostsButton(){
@@ -513,19 +365,20 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(currentNewPostsAnim!=null){ if(currentNewPostsAnim!=null){
currentNewPostsAnim.cancel(); currentNewPostsAnim.cancel();
} }
newPostsBtnWrap.setVisibility(View.VISIBLE); toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
AnimatorSet set=new AnimatorSet(); AnimatorSet set=new AnimatorSet();
set.playTogether( set.playTogether(
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 1f), ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, 1f), ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, 1f), ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, 0f) ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
); );
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3)); set.setDuration(300);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_decelerate)); set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){ set.addListener(new AnimatorListenerAdapter(){
@Override @Override
public void onAnimationEnd(Animator animation){ public void onAnimationEnd(Animator animation){
toolbarLogo.setVisibility(View.INVISIBLE);
currentNewPostsAnim=null; currentNewPostsAnim=null;
} }
}); });
@@ -540,19 +393,20 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(currentNewPostsAnim!=null){ if(currentNewPostsAnim!=null){
currentNewPostsAnim.cancel(); currentNewPostsAnim.cancel();
} }
toolbarLogo.setVisibility(View.VISIBLE);
AnimatorSet set=new AnimatorSet(); AnimatorSet set=new AnimatorSet();
set.playTogether( set.playTogether(
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 0f), ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, .9f), ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, .9f), ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, V.dp(-56)) ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
); );
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3)); set.setDuration(300);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_accelerate)); set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){ set.addListener(new AnimatorListenerAdapter(){
@Override @Override
public void onAnimationEnd(Animator animation){ public void onAnimationEnd(Animator animation){
newPostsBtnWrap.setVisibility(View.GONE); toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
currentNewPostsAnim=null; currentNewPostsAnim=null;
} }
}); });
@@ -567,20 +421,6 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
} }
} }
private void onListsDropdownClick(View v){
listsDropdownArrow.animate().rotation(-180f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
dropdownController.show(dropdownMainMenuController);
AccountSessionManager.get(accountID).getCacheController().reloadLists(new Callback<>(){
@Override
public void onSuccess(java.util.List<FollowList> result){
lists=result;
}
@Override
public void onError(ErrorResponse error){}
});
}
@Override @Override
public void onDestroyView(){ public void onDestroyView(){
super.onDestroyView(); super.onDestroyView();
@@ -603,67 +443,4 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){ protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true; return true;
} }
@Override
public Toolbar getToolbar(){
return super.getToolbar();
}
@Override
public void onDropdownWillDismiss(){
listsDropdownArrow.animate().rotation(0f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
}
@Override
public void onDropdownDismissed(){
}
@Override
public void reload(){
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
refreshing=true;
showProgress();
loadData();
listsDropdownText.setText(getCurrentListTitle());
invalidateOptionsMenu();
}
@Override
protected RecyclerView.Adapter getAdapter(){
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
protected void onDataLoaded(List<Status> d, boolean more){
if(refreshing){
if(listMode==ListMode.LOCAL){
localTimelineBannerHelper.maybeAddBanner(list, mergeAdapter);
localTimelineBannerHelper.onBannerBecameVisible();
}else{
localTimelineBannerHelper.removeBanner(mergeAdapter);
}
}
super.onDataLoaded(d, more);
}
private String getCurrentListTitle(){
return switch(listMode){
case FOLLOWING -> getString(R.string.timeline_following);
case LOCAL -> getString(R.string.local_timeline);
case LIST -> currentList.title;
};
}
private enum ListMode{
FOLLOWING,
LOCAL,
LIST
}
} }

View File

@@ -1,301 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.ActionModeHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
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.utils.V;
public class ListMembersFragment extends PaginatedAccountListFragment{
private static final int ADD_MEMBER_RESULT=600;
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
private Set<String> selectedAccounts=new HashSet<>();
private ActionMode actionMode;
private MenuItem deleteItem;
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
setTitle(R.string.list_members);
setHasOptionsMenu(true);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetListAccounts(followList.id, maxID, count);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
holder.setOnClickListener(this::onItemClick);
holder.setOnLongClickListener(this::onItemLongClick);
holder.getContextMenu().getMenu().add(0, R.id.remove_from_list, 0, R.string.remove_from_list);
holder.setOnCustomMenuItemSelectedListener(item->onItemMenuItemSelected(holder, item));
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
super.onBindViewHolder(holder);
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
if(inSelectionMode){
holder.setChecked(selectedAccounts.contains(holder.getItem().account.id));
}
}
@Override
public boolean wantsLightStatusBar(){
if(actionMode!=null)
return UiUtils.isDarkTheme();
return super.wantsLightStatusBar();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.selectable_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.select){
enterSelectionMode();
}else if(id==R.id.select_all){
for(AccountViewModel a:data){
selectedAccounts.add(a.account.id);
}
enterSelectionMode();
}
return true;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px);
fab.setContentDescription(getString(R.string.add_list_member));
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==ADD_MEMBER_RESULT && success){
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
addAccounts(List.of(acc));
}
}
@Subscribe
public void onAccountRemovedFromList(AccountRemovedFromListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
removeAccountRows(Set.of(ev.targetAccountID));
}
}
@Subscribe
public void onAccountAddedToList(AccountAddedToListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
data.add(new AccountViewModel(ev.account, accountID));
list.getAdapter().notifyItemInserted(data.size()-1);
}
}
private void onFabClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
}
private void onItemClick(AccountViewHolder holder){
if(inSelectionMode){
String id=holder.getItem().account.id;
if(selectedAccounts.contains(id)){
selectedAccounts.remove(id);
holder.setChecked(false);
}else{
selectedAccounts.add(id);
holder.setChecked(true);
}
updateActionModeTitle();
deleteItem.setEnabled(!selectedAccounts.isEmpty());
return;
}
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(holder.getItem().account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private boolean onItemLongClick(AccountViewHolder holder){
if(inSelectionMode)
return false;
selectedAccounts.add(holder.getItem().account.id);
enterSelectionMode();
return true;
}
private void onItemMenuItemSelected(AccountViewHolder holder, MenuItem item){
int id=item.getItemId();
if(id==R.id.remove_from_list){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
private void updateItemsForSelectionModeTransition(){
list.getAdapter().notifyItemRangeChanged(0, data.size());
}
private void enterSelectionMode(){
inSelectionMode=true;
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.INVISIBLE);
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
deleteItem=menu.findItem(R.id.delete);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
.setNegativeButton(R.string.cancel, null)
.show();
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode){
actionMode=null;
inSelectionMode=false;
selectedAccounts.clear();
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.VISIBLE);
}
});
updateActionModeTitle();
}
private void updateActionModeTitle(){
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
}
private void removeAccounts(Set<String> ids){
new RemoveAccountsFromList(followList.id, ids)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(inSelectionMode)
actionMode.finish();
removeAccountRows(ids);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void addAccounts(Collection<Account> accounts){
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
for(Account acc:accounts){
data.add(new AccountViewModel(acc, accountID));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
private void removeAccountRows(Set<String> ids){
for(int i=data.size()-1;i>=0;i--){
if(ids.contains(data.get(i).account.id)){
data.remove(i);
list.getAdapter().notifyItemRemoved(i);
}
}
}
}

View File

@@ -1,61 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ListTimelineFragment extends StatusListFragment{
private FollowList followList;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
followList=Parcels.unwrap(getArguments().getParcelable("list"));
setTitle(followList.title);
setHasOptionsMenu(true);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetListTimeline(followList.id, offset>0 ? getMaxID() : null, null, count, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.standalone_list_timeline, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(followList));
if(id==R.id.members){
Nav.go(getActivity(), ListMembersFragment.class, args);
}else if(id==R.id.edit_list){
Nav.go(getActivity(), EditListFragment.class, args);
}
return true;
}
}

View File

@@ -1,95 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
private String maxID;
public ManageFollowedHashtagsFragment(){
super(100);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_hashtags);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
maxID=null;
if(result.nextPageUri!=null)
maxID=result.nextPageUri.getQueryParameter("max_id");
onDataLoaded(result.stream().map(t->{
int posts=t.getWeekPosts();
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
R.drawable.ic_tag_24px, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
}).collect(Collectors.toList()), maxID!=null);
}
})
.exec(accountID);
}
@Override
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
menu.clear();
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
}
@Override
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
new M3AlertDialogBuilder(getActivity())
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
}
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
new SetTagFollowed(item.parentObject.name, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Hashtag result){
int index=data.indexOf(item);
if(index==-1)
return;
data.remove(index);
list.getAdapter().notifyItemRemoved(index);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
}

View File

@@ -1,199 +0,0 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ListCreatedEvent;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedEvent;
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Comparator;
import java.util.List;
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.api.SimpleCallback;
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
private ImageButton fab;
public ManageListsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.manage_lists);
loadData();
setRefreshEnabled(true);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected void doLoadData(int offset, int count){
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowList> result){
onDataLoaded(result.stream().map(ManageListsFragment.this::makeItem).collect(Collectors.toList()), false);
}
};
if(refreshing){
AccountSessionManager.get(accountID)
.getCacheController()
.reloadLists(callback);
}else{
AccountSessionManager.get(accountID)
.getCacheController()
.getLists(callback);
}
}
private ListItem<FollowList> makeItem(FollowList l){
return new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false);
}
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(item.parentObject));
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
@Override
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
menu.add(0, R.id.edit, 0, R.string.edit_list);
menu.add(0, R.id.delete, 1, R.string.delete_list);
}
@Override
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
int id=menuItem.getItemId();
if(id==R.id.edit){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("list", Parcels.wrap(item.parentObject));
Nav.go(getActivity(), EditListFragment.class, args);
}else if(id==R.id.delete){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.delete_list)
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px);
fab.setContentDescription(getString(R.string.create_list));
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
private void doDeleteList(FollowList list){
new DeleteList(list.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
for(int i=0;i<data.size();i++){
if(data.get(i).parentObject==list){
data.remove(i);
itemsAdapter.notifyItemRemoved(i);
AccountSessionManager.get(accountID).getCacheController().deleteList(list.id);
break;
}
}
}
@Override
public void onError(ErrorResponse error){
Activity activity=getActivity();
if(activity==null)
return;
error.showToast(activity);
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
@Subscribe
public void onListUpdated(ListUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
for(ListItem<FollowList> item:data){
if(item.parentObject.id.equals(ev.list.id)){
item.parentObject=ev.list;
item.title=ev.list.title;
rebindItem(item);
break;
}
}
}
@Subscribe
public void onListDeleted(ListDeletedEvent ev){
if(!ev.accountID.equals(accountID))
return;
int i=0;
for(ListItem<FollowList> item:data){
if(item.parentObject.id.equals(ev.listID)){
data.remove(i);
itemsAdapter.notifyItemRemoved(i);
break;
}
i++;
}
}
@Subscribe
public void onListCreated(ListCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
ListItem<FollowList> item=makeItem(ev.list);
data.add(item);
((List<ListItem<FollowList>>)data).sort(Comparator.comparing(l->l.parentObject.title));
itemsAdapter.notifyItemInserted(data.indexOf(item));
}
private void onFabClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), CreateListFragment.class, args);
}
}

View File

@@ -2,10 +2,13 @@ package org.joinmastodon.android.fragments;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -36,21 +39,22 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
@CallSuper @CallSuper
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
if(wantsElevationOnScrollEffect()){ if(wantsElevationOnScrollEffect())
FragmentRootLinearLayout rootView; list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
if(view instanceof FragmentRootLinearLayout frl)
rootView=frl;
else
rootView=view.findViewById(R.id.appkit_loader_root);
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener(rootView, getViewsForElevationEffect()));
}
list.setItemAnimator(new BetterItemAnimator());
if(refreshLayout!=null){ if(refreshLayout!=null){
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background); int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary); int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f)); refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
refreshLayout.setColorSchemeColors(colorPrimary); refreshLayout.setColorSchemeColors(colorPrimary);
} }
// This is to set the color of the 'This list is empty'
for (int i=0; i < ((LinearLayout) emptyView).getChildCount(); i++) {
View v = ((LinearLayout) emptyView).getChildAt(i);
if(v instanceof TextView) {
((TextView) v).setTextColor(UiUtils.getThemeColor(getContext(), android.R.attr.textColorSecondary));
}
}
} }
@Override @Override

View File

@@ -44,7 +44,7 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{ public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions; private boolean onlyMentions=true;
private String maxID; private String maxID;
private View tabBar; private View tabBar;
private View mentionsTab, allTab; private View mentionsTab, allTab;
@@ -58,7 +58,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setLayout(R.layout.fragment_notifications); setLayout(R.layout.fragment_notifications);
E.register(this); E.register(this);
onlyMentions=AccountSessionManager.get(accountID).isNotificationsMentionsOnly(); if(savedInstanceState!=null){
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
}
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@@ -130,9 +132,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected void onShown(){ protected void onShown(){
super.onShown(); super.onShown();
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker(); unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
if(!dataLoading && canRefreshWithoutUpsettingUser()){ if(!dataLoading){
reloadingFromCache=true; if(onlyMentions){
refresh(); refresh();
}else{
reloadingFromCache=true;
refresh();
}
} }
} }
@@ -215,6 +221,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return views; return views;
} }
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("onlyMentions", onlyMentions);
}
private Notification getNotificationByID(String id){ private Notification getNotificationByID(String id){
for(Notification n:data){ for(Notification n:data){
if(n.id.equals(id)) if(n.id.equals(id))
@@ -279,10 +291,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
allTab.setSelected(!onlyMentions); allTab.setSelected(!onlyMentions);
maxID=null; maxID=null;
showProgress(); showProgress();
refreshing=true;
reloadingFromCache=true;
loadData(0, 20); loadData(0, 20);
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions); refreshing=true;
} }
@Override @Override
@@ -302,6 +312,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu); inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read); markAllReadItem=menu.findItem(R.id.mark_all_read);
updateMarkAllReadButton();
} }
@Override @Override
@@ -314,13 +325,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
} }
private void markAsRead(){ private void markAsRead(){
if(data.isEmpty())
return;
String id=data.get(0).id; String id=data.get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){ if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID); new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true); AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id; realUnreadMarker=id;
updateMarkAllReadButton();
} }
} }
@@ -338,6 +348,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}); });
} }
private void updateMarkAllReadButton(){
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
}
@Override @Override
public void onAppendItems(List<Notification> items){ public void onAppendItems(List<Notification> items){
super.onAppendItems(items); super.onAppendItems(items);
@@ -350,20 +364,4 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
} }
} }
} }
private boolean canRefreshWithoutUpsettingUser(){
// TODO maybe reload notifications the same way we reload the home timelines, i.e. with gaps and stuff
if(data.size()<=itemsPerPage)
return true;
for(int i=list.getChildCount()-1;i>=0;i--){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
String id=itemHolder.getItemID();
for(int j=0;j<data.size();j++){
if(data.get(j).id.equals(id))
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
}
}
}
return true;
}
} }

View File

@@ -40,6 +40,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
@@ -62,12 +63,10 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener; import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView; import org.joinmastodon.android.ui.views.CoverImageView;
@@ -109,7 +108,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar; private ImageView avatar;
private CoverImageView cover; private CoverImageView cover;
private View avatarBorder; private View avatarBorder;
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel; private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private ProgressBarButton actionButton; private ProgressBarButton actionButton;
private ViewPager2 pager; private ViewPager2 pager;
private NestedRecyclerScrollView scrollView; private NestedRecyclerScrollView scrollView;
@@ -187,7 +186,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatarBorder=content.findViewById(R.id.avatar_border); avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name); name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username); username=content.findViewById(R.id.username);
usernameDomain=content.findViewById(R.id.username_domain);
bio=content.findViewById(R.id.bio); bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count); followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label); followersLabel=content.findViewById(R.id.followers_label);
@@ -303,7 +301,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain; username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
} }
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username)); getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
UiUtils.maybeShowTextCopiedToast(getActivity()); if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
}
return true; return true;
}); });
@@ -323,8 +323,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true)); nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true)); bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
return sizeWrapper; return sizeWrapper;
} }
@@ -504,22 +502,23 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account); boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){ if(account.locked){
ssb=new SpannableStringBuilder(account.username); ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb.append(" "); ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate(); Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight()); lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor()); lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0); ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
username.setText(ssb); username.setText(ssb);
}else{ }else{
username.setText(account.username); // noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
} }
String domain=account.getDomain(); CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
if(TextUtils.isEmpty(parsedBio)){ if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE); bio.setVisibility(View.GONE);
}else{ }else{
@@ -554,7 +553,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
fields.add(joined); fields.add(joined);
for(AccountField field:account.fields){ for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account); field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class); field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name); ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis); HtmlParser.parseCustomEmoji(ssb, account.emojis);
@@ -610,7 +609,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())); menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
else else
menu.findItem(R.id.block_domain).setVisible(false); menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
} }
@Override @Override
@@ -634,10 +632,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.open_in_browser){ }else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url); UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){ }else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account, relationship.domainBlocking, ()->{ UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking; relationship.domainBlocking=!relationship.domainBlocking;
updateRelationship(); updateRelationship();
}, this::updateRelationship); });
}else if(id==R.id.hide_boosts){ }else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs) new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@@ -664,11 +662,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.save){ }else if(id==R.id.save){
if(isInEditMode) if(isInEditMode)
saveAndExitEditMode(); saveAndExitEditMode();
}else if(id==R.id.add_to_list){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
} }
return true; return true;
} }
@@ -857,7 +850,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
aboutFragment.enterEditMode(account.source.fields); aboutFragment.enterEditMode(account.source.fields);
refreshLayout.setEnabled(false); refreshLayout.setEnabled(false);
editDirty=false; editDirty=false;
V.setVisibilityAnimated(fab, View.GONE);
} }
private void exitEditMode(){ private void exitEditMode(){
@@ -900,7 +892,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
refreshLayout.setEnabled(true); refreshLayout.setEnabled(true);
bindHeaderView(); bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);
} }
private void saveAndExitEditMode(){ private void saveAndExitEditMode(){
@@ -980,7 +971,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return; return;
int radius=V.dp(25); int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0, currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null)); new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
} }
} }
@@ -992,7 +983,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(drawable==null || drawable instanceof ColorDrawable) if(drawable==null || drawable instanceof ColorDrawable)
return; return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0, 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))); new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
} }
} }
@@ -1053,7 +1044,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@NonNull @NonNull
@Override @Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=new FrameLayout(parent.getContext()); FrameLayout view=tabViews[viewType];
((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view); return new SimpleViewHolder(view);
} }
@@ -1061,13 +1054,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override @Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){ public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Fragment fragment=getFragmentForPage(position); Fragment fragment=getFragmentForPage(position);
FrameLayout fragmentView=tabViews[position];
fragmentView.setVisibility(View.VISIBLE);
if(fragmentView.getParent() instanceof ViewGroup parent)
parent.removeView(fragmentView);
((FrameLayout)holder.itemView).addView(fragmentView);
if(!fragment.isAdded()){ if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(fragmentView.getId(), fragment).commit(); getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override @Override
public boolean onPreDraw(){ public boolean onPreDraw(){

View File

@@ -1,12 +1,7 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -16,8 +11,6 @@ import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances; import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
@@ -27,7 +20,6 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance; import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -55,15 +47,13 @@ public class SplashFragment extends AppKitFragment{
private ProgressBarButton defaultServerButton; private ProgressBarButton defaultServerButton;
private ProgressBar defaultServerProgress; private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER; private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer, loadedDefaultServer; private boolean loadingDefaultServer;
private Uri currentInviteLink;
private ProgressDialog instanceLoadingProgress;
private String inviteCode;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context); motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
loadAndChooseDefaultServer();
} }
@Nullable @Nullable
@@ -111,8 +101,6 @@ public class SplashFragment extends AppKitFragment{
}); });
} }
}); });
if(!loadedDefaultServer && !loadingDefaultServer)
loadAndChooseDefaultServer();
return contentView; return contentView;
} }
@@ -121,65 +109,19 @@ public class SplashFragment extends AppKitFragment{
Bundle extras=new Bundle(); Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started; boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup); extras.putBoolean("signup", isSignup);
extras.putString("defaultServer", chosenDefaultServer);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras); Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
} }
private void onJoinDefaultServerClick(View v){ private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer) if(loadingDefaultServer)
return; return;
instanceLoadingProgress=new ProgressDialog(getActivity());
instanceLoadingProgress.setCancelable(false);
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
instanceLoadingProgress.show();
if(currentInviteLink!=null){
new CheckInviteLink(currentInviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCode=result.inviteCode;
proceedWithServerDomain(currentInviteLink.getHost());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.execNoAuth(currentInviteLink.getHost());
return;
}
proceedWithServerDomain(chosenDefaultServer);
}
private void proceedWithServerDomain(String domain){
new GetInstance() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance result){
if(getActivity()==null) if(getActivity()==null)
return; return;
instanceLoadingProgress.dismiss(); if(!result.registrations){
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed) .setMessage(R.string.instance_signup_closed)
@@ -189,8 +131,6 @@ public class SplashFragment extends AppKitFragment{
} }
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result)); args.putParcelable("instance", Parcels.wrap(result));
if(inviteCode!=null)
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
} }
@@ -198,12 +138,11 @@ public class SplashFragment extends AppKitFragment{
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
if(getActivity()==null) if(getActivity()==null)
return; return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
error.showToast(getActivity()); error.showToast(getActivity());
} }
}) })
.execNoAuth(domain); .wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(chosenDefaultServer);
} }
private void onLearnMoreClick(View v){ private void onLearnMoreClick(View v){
@@ -258,18 +197,7 @@ public class SplashFragment extends AppKitFragment{
} }
private void loadAndChooseDefaultServer(){ private void loadAndChooseDefaultServer(){
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip(); loadingDefaultServer=true;
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
currentInviteLink=Uri.parse(clipText.toString());
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
}
}else{
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
new GetCatalogDefaultInstances() new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
@@ -311,8 +239,7 @@ public class SplashFragment extends AppKitFragment{
private void setChosenDefaultServer(String domain){ private void setChosenDefaultServer(String domain){
chosenDefaultServer=domain; chosenDefaultServer=domain;
loadingDefaultServer=false; loadingDefaultServer=false;
loadedDefaultServer=true; if(defaultServerButton!=null && getActivity()!=null){
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true); defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE); defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer)); defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));

View File

@@ -2,24 +2,19 @@ package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext; import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext; import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
@@ -31,12 +26,10 @@ import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@@ -44,16 +37,10 @@ import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{ public class ThreadFragment extends StatusListFragment{
private Status mainStatus; private Status mainStatus;
private ImageView endMark; private ImageView endMark;
private FrameLayout replyContainer;
private LinearLayout replyButton;
private ImageView replyButtonAva;
private TextView replyButtonText;
private int lastBottomInset;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setLayout(R.layout.fragment_thread);
mainStatus=Parcels.unwrap(getArguments().getParcelable("status")); mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount")); Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
if(inReplyToAccount!=null) if(inReplyToAccount!=null)
@@ -82,7 +69,7 @@ public class ThreadFragment extends StatusListFragment{
} }
} }
} }
items.add(items.size()-1, new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus())); items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
} }
return items; return items;
} }
@@ -139,20 +126,6 @@ public class ThreadFragment extends StatusListFragment{
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
replyContainer=view.findViewById(R.id.reply_button_wrapper);
replyButton=replyContainer.findViewById(R.id.reply_button);
replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
replyButtonAva=replyButton.findViewById(R.id.avatar);
replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
replyButton.setClipToOutline(true);
replyButtonText.setText(getString(R.string.reply_to_user, mainStatus.account.displayName));
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
replyButtonAva.setClipToOutline(true);
replyButton.setOnClickListener(v->openReply());
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
}
UiUtils.loadCustomEmojiInTextView(toolbarTitleView); UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent(); showContent();
if(!loaded) if(!loaded)
@@ -202,24 +175,4 @@ public class ThreadFragment extends StatusListFragment{
} }
super.onErrorRetryClick(); super.onErrorRetryClick();
} }
@Override
public void onApplyWindowInsets(WindowInsets insets){
lastBottomInset=insets.getSystemWindowInsetBottom();
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
}
private void openReply(){
maybeShowPreReplySheet(mainStatus, ()->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
args.putBoolean("fromThreadFragment", true);
Nav.go(getActivity(), ComposeFragment.class, args);
});
}
public int getSnackbarOffset(){
return replyContainer.getHeight()-lastBottomInset;
}
} }

View File

@@ -1,37 +0,0 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.model.Account;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class AddListMembersFragment extends AccountSearchFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
}

View File

@@ -1,125 +0,0 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Button;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
public class AddNewListMembersFragment extends AccountSearchFragment{
private Listener listener;
private String maxID;
public AddNewListMembersFragment(Listener listener){
this.listener=listener;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
if(TextUtils.isEmpty(currentQuery)){
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, offset>0 ? maxID : null, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
setEmptyText("");
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
maxID=result.getNextPageMaxID();
}
})
.exec(accountID);
}else{
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddNewListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
holder.setOnLongClickListener(vh->false);
Button button=holder.getButton();
button.setPadding(V.dp(24), 0, V.dp(24), 0);
button.setMinimumWidth(0);
button.setMinWidth(0);
button.setOnClickListener(v->{
holder.setActionProgressVisible(true);
holder.itemView.setHasTransientState(true);
Runnable onDone=()->{
holder.setActionProgressVisible(false);
holder.itemView.setHasTransientState(false);
onBindViewHolder(holder);
};
AccountViewModel account=holder.getItem();
if(listener.isAccountInList(account)){
listener.removeAccountAccountFromList(account, onDone);
}else{
listener.addAccountToList(account, onDone);
}
});
}
@Override
protected void onBindViewHolder(AccountViewHolder holder){
Button button=holder.getButton();
int textRes, styleRes;
if(listener.isAccountInList(holder.getItem())){
textRes=R.string.remove;
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
}else{
textRes=R.string.add;
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}
button.setText(textRes);
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
button.setTextColor(ta.getColorStateList(0));
ta.recycle();
}
@Override
protected void loadRelationships(List<AccountViewModel> accounts){
// no-op
}
public interface Listener{
boolean isAccountInList(AccountViewModel account);
void addAccountToList(AccountViewModel account, Runnable onDone);
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
}
}

View File

@@ -38,7 +38,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID; protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>(); protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
protected int itemLayoutRes=R.layout.item_account_list;
public BaseAccountListFragment(){ public BaseAccountListFragment(){
super(40); super(40);
@@ -74,8 +73,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected void loadRelationships(List<AccountViewModel> accounts){ protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet()); Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
if(ids.isEmpty())
return;
GetAccountRelationships req=new GetAccountRelationships(ids); GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req); relationshipsRequests.add(req);
req.setCallback(new Callback<>(){ req.setCallback(new Callback<>(){
@@ -125,9 +122,20 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
Toolbar toolbar=getToolbar(); Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){ if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back); toolbar.setNavigationContentDescription(R.string.back);
if(hasSubtitle()){
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
toolbar.setTitleTextColor(color);
toolbar.setSubtitleTextColor(color);
}
} }
} }
protected boolean hasSubtitle(){
return true;
}
@Override @Override
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){ if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
@@ -142,7 +150,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
} }
protected void onConfigureViewHolder(AccountViewHolder holder){} protected void onConfigureViewHolder(AccountViewHolder holder){}
protected void onBindViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){
@@ -152,7 +159,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull @NonNull
@Override @Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes); AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder); onConfigureViewHolder(holder);
return holder; return holder;
} }
@@ -160,7 +167,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@Override @Override
public void onBindViewHolder(AccountViewHolder holder, int position){ public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position)); holder.bind(data.get(position));
BaseAccountListFragment.this.onBindViewHolder(holder);
super.onBindViewHolder(holder, position); super.onBindViewHolder(holder, position);
} }

View File

@@ -5,9 +5,7 @@ import android.text.TextUtils;
import android.view.View; import android.view.View;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.search.GetSearchResults; import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.SearchResults; import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel; import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.SearchViewHelper; import org.joinmastodon.android.ui.SearchViewHelper;
@@ -15,14 +13,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder; import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
public class AccountSearchFragment extends BaseAccountListFragment{ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
protected String currentQuery; private String currentQuery;
private boolean resultDelivered; private boolean resultDelivered;
private SearchViewHelper searchViewHelper; private SearchViewHelper searchViewHelper;
@@ -31,11 +28,12 @@ public class AccountSearchFragment extends BaseAccountListFragment{
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setRefreshEnabled(false); setRefreshEnabled(false);
setEmptyText(""); setEmptyText("");
dataLoaded();
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder()); searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper.setListeners(this::onQueryChanged, null); searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView); searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
@@ -53,21 +51,13 @@ public class AccountSearchFragment extends BaseAccountListFragment{
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(SearchResults result){ public void onSuccess(SearchResults result){
AccountSearchFragment.this.onSuccess(result.accounts); setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
} }
}) })
.exec(accountID); .exec(accountID);
} }
protected void onSuccess(List<Account> result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
protected String getSearchViewPlaceholder(){
return getString(R.string.search_hint);
}
@Override @Override
protected void onUpdateToolbar(){ protected void onUpdateToolbar(){
super.onUpdateToolbar(); super.onUpdateToolbar();

View File

@@ -14,4 +14,8 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
status=Parcels.unwrap(getArguments().getParcelable("status")); status=Parcels.unwrap(getArguments().getParcelable("status"));
} }
@Override
protected boolean hasSubtitle(){
return false;
}
} }

View File

@@ -49,6 +49,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private DiscoverNewsFragment newsFragment; private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment; private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment; private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment;
private String accountID; private String accountID;
private String currentQuery; private String currentQuery;
@@ -70,14 +71,15 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar); tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager); pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[4]; tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){ for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity()); FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){ tabView.setId(switch(i){
case 0 -> R.id.discover_posts; case 0 -> R.id.discover_posts;
case 1 -> R.id.discover_hashtags; case 1 -> R.id.discover_hashtags;
case 2 -> R.id.discover_news; case 2 -> R.id.discover_news;
case 3 -> R.id.discover_users; case 3 -> R.id.discover_local_timeline;
case 4 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i); default -> throw new IllegalStateException("Unexpected value: "+i);
}); });
tabView.setVisibility(View.GONE); tabView.setVisibility(View.GONE);
@@ -120,8 +122,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment(); accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args); accountsFragment.setArguments(args);
localTimelineFragment=new LocalTimelineFragment();
localTimelineFragment.setArguments(args);
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment) .add(R.id.discover_posts, postsFragment)
.add(R.id.discover_local_timeline, localTimelineFragment)
.add(R.id.discover_hashtags, hashtagsFragment) .add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment) .add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment) .add(R.id.discover_users, accountsFragment)
@@ -135,7 +141,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 0 -> R.string.posts; case 0 -> R.string.posts;
case 1 -> R.string.hashtags; case 1 -> R.string.hashtags;
case 2 -> R.string.news; case 2 -> R.string.news;
case 3 -> R.string.for_you; case 3 -> R.string.local_timeline;
case 4 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position); default -> throw new IllegalStateException("Unexpected value: "+position);
}); });
} }
@@ -238,7 +245,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 0 -> postsFragment; case 0 -> postsFragment;
case 1 -> hashtagsFragment; case 1 -> hashtagsFragment;
case 2 -> newsFragment; case 2 -> newsFragment;
case 3 -> accountsFragment; case 3 -> localTimelineFragment;
case 4 -> accountsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page); default -> throw new IllegalStateException("Unexpected value: "+page);
}; };
} }
@@ -272,19 +280,15 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
@NonNull @NonNull
@Override @Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=new FrameLayout(parent.getContext()); FrameLayout view=tabViews[viewType];
((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view); return new SimpleViewHolder(view);
} }
@Override @Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){ public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
FrameLayout view=tabViews[position];
if(view.getParent() instanceof ViewGroup parent)
parent.removeView(view);
view.setVisibility(View.VISIBLE);
((FrameLayout)holder.itemView).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override @Override
public int getItemCount(){ public int getItemCount(){

View File

@@ -3,9 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.os.Bundle; import android.os.Bundle;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses; import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
@@ -17,7 +15,6 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment{ public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper; private DiscoverInfoBannerHelper bannerHelper;
private int realOffset=0;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -27,12 +24,10 @@ public class DiscoverPostsFragment extends StatusListFragment{
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetTrendingStatuses(offset==0 ? 0 : realOffset, count) currentRequest=new GetTrendingStatuses(offset, count)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
realOffset+=result.size();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible(); bannerHelper.onBannerBecameVisible();
} }

View File

@@ -0,0 +1,54 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class LocalTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
private String maxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
}

View File

@@ -32,7 +32,6 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
private String currentQuery; private String currentQuery;
@@ -138,7 +137,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}*/ }*/
int offset=_offset; int offset=_offset;
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count) currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
.setCallback(new SimpleCallback<SearchResults>(this){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(SearchResults result){ public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>(); ArrayList<SearchResult> results=new ArrayList<>();
@@ -159,10 +158,16 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
} }
prevDisplayItems=new ArrayList<>(displayItems); prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results; unfilteredResults=results;
boolean wasRefreshing=refreshing;
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty()); onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
if(wasRefreshing) }
list.scrollToPosition(0);
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
} }
}) })
.exec(accountID); .exec(accountID);

View File

@@ -112,7 +112,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
onDataLoaded(results.stream().map(sr->{ onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true); SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){ if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr)); vm.hashtagItem.onClick=()->openHashtag(sr);
} }
return vm; return vm;
}).collect(Collectors.toList()), false); }).collect(Collectors.toList()), false);
@@ -129,7 +129,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
.map(sr->{ .map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false); SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){ if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr)); vm.hashtagItem.onClick=()->openHashtag(sr);
} }
return vm; return vm;
}) })
@@ -384,23 +384,21 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
} }
private void onSearchViewEnter(){ private void onSearchViewEnter(){
if(TextUtils.isEmpty(currentQuery) || currentQuery.trim().isEmpty())
return;
deliverResult(currentQuery, null); deliverResult(currentQuery, null);
} }
private void onOpenURLClick(ListItem<?> item_){ private void onOpenURLClick(){
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID); ((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
} }
private void onGoToHashtagClick(ListItem<?> item_){ private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery(); String q=searchViewHelper.getQuery();
if(q.startsWith("#")) if(q.startsWith("#"))
q=q.substring(1); q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q); UiUtils.openHashtagTimeline(getActivity(), accountID, q);
} }
private void onGoToAccountClick(ListItem<?> item_){ private void onGoToAccountClick(){
String q=searchViewHelper.getQuery(); String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){ if(!q.startsWith("@")){
q="@"+q; q="@"+q;
@@ -408,14 +406,14 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
if(q.lastIndexOf('@')==0){ if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain; q+="@"+AccountSessionManager.get(accountID).domain;
} }
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true, GetSearchResults.Type.ACCOUNTS); ((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
} }
private void onGoToStatusSearchClick(ListItem<?> item_){ private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS); deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
} }
private void onGoToAccountSearchClick(ListItem<?> item_){ private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT); deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
} }

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment; import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File; import java.io.File;
@@ -165,7 +165,9 @@ public class AccountActivationFragment extends ToolbarFragment{
private void tryGetAccount(){ private void tryGetAccount(){
if(AccountSessionManager.getInstance().tryGetAccount(accountID)==null){ if(AccountSessionManager.getInstance().tryGetAccount(accountID)==null){
uiHandler.removeCallbacks(pollRunnable); uiHandler.removeCallbacks(pollRunnable);
((MainActivity)getActivity()).restartHomeFragment(); getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
return; return;
} }
currentRequest=new GetOwnAccount() currentRequest=new GetOwnAccount()

View File

@@ -137,9 +137,6 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
protected void onButtonClick(){ protected void onButtonClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this); Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
} }

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -36,7 +37,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
@@ -48,6 +48,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@@ -60,7 +61,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected EditText searchEdit; protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced; protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery; protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain; protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>(); protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar; protected View buttonBar;
@@ -91,7 +91,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN) if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true; return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList(); updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer); searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery)); Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -106,7 +105,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected void onSearchChangedDebounced(){ protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList(); updateFilteredList();
loadInstanceInfo(currentSearchQuery, false); loadInstanceInfo(currentSearchQuery, false);
} }
@@ -151,10 +149,6 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
} }
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){ protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
loadInstanceInfo(_domain, isFromRedirect, null);
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain)) if(TextUtils.isEmpty(_domain))
return; return;
String domain=normalizeInstanceDomain(_domain); String domain=normalizeInstanceDomain(_domain);
@@ -179,10 +173,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
try{ try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){ }catch(URISyntaxException x){
if(onError!=null) showInstanceInfoLoadError(domain, x);
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){ if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error); fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -202,11 +193,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=null; loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result); instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){ if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss(); instanceProgressDialog.dismiss();
instanceProgressDialog=null; instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
} }
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){ if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false; boolean found=false;
@@ -233,14 +223,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
loadingInstanceRequest=null; loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){ if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError); fetchDomainFromHostMetaAndMaybeRetry(domain, error);
return; return;
} }
loadingInstanceDomain=null; loadingInstanceDomain=null;
if(onError!=null) showInstanceInfoLoadError(domain, error);
onError.accept(error);
else
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){ if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error); fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -289,7 +276,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
} }
} }
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){ private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
String url="https://"+domain+"/.well-known/host-meta"; String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder() Request req=new Request.Builder()
.url(url) .url(url)
@@ -303,12 +290,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
Activity a=getActivity(); Activity a=getActivity();
if(a==null) if(a==null)
return; return;
a.runOnUiThread(()->{ a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
} }
@Override @Override
@@ -320,13 +302,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return; return;
try(response){ try(response){
if(!response.isSuccessful()){ if(!response.isSuccessful()){
a.runOnUiThread(()->{ a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
String err=response.code()+" "+response.message();
if(onError!=null)
onError.accept(err);
else
showInstanceInfoLoadError(domain, err);
});
return; return;
} }
InputSource source=new InputSource(response.body().charStream()); InputSource source=new InputSource(response.body().charStream());
@@ -345,19 +321,9 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
} }
} }
} }
a.runOnUiThread(()->{ a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
if(onError!=null)
onError.accept(origError);
else
showInstanceInfoLoadError(domain, origError);
});
}catch(Exception x){ }catch(Exception x){
a.runOnUiThread(()->{ a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
});
} }
} }
}); });

View File

@@ -1,13 +1,8 @@
package org.joinmastodon.android.fragments.onboarding; package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
@@ -17,8 +12,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@@ -26,12 +19,9 @@ import android.widget.PopupMenu;
import android.widget.RadioButton; import android.widget.RadioButton;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest; 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.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
@@ -39,8 +29,6 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView; import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -52,9 +40,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -91,9 +77,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private CatalogInstance.Region chosenRegion; private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL; private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
public InstanceCatalogSignupFragment(){ public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10); super(R.layout.fragment_onboarding_common, 10);
} }
@@ -334,7 +317,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
focusThing=view.findViewById(R.id.focus_thing); focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus(); focusThing.requestFocus();
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick); view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
nextButton.setEnabled(chosenInstance!=null); nextButton.setEnabled(chosenInstance!=null);
} }
@@ -368,191 +351,91 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override @Override
protected void proceedWithAuthOrSignup(Instance instance){ protected void proceedWithAuthOrSignup(Instance instance){
if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCodeHost=inviteLink.getHost();
inviteCode=result.inviteCode;
proceedWithAuthOrSignup(instance);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(inviteLink.getHost());
return;
}
}
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){ if(!instance.registrations){
if(instance.invitesEnabled){ new M3AlertDialogBuilder(getActivity())
showInviteLinkAlert(instance.uri); .setTitle(R.string.error)
}else{ .setMessage(R.string.instance_signup_closed)
new M3AlertDialogBuilder(getActivity()) .setPositiveButton(R.string.ok, null)
.setTitle(R.string.error) .show();
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
}
return; return;
} }
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(instance));
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
} }
private void onUseInviteClick(View v){ private void onPickRandomInstanceClick(View v){
showInviteLinkAlert(null); String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
} }
private void showInviteLinkAlert(String domain){ // private String getEmojiForCategory(String category){
AlertDialog alert=new M3AlertDialogBuilder(getActivity()) // return switch(category){
.setView(R.layout.alert_invite_link) // case "all" -> "💬";
.setPositiveButton(R.string.next, null) // case "academia" -> "📚";
.setNegativeButton(R.string.cancel, null) // case "activism" -> "✊";
.create(); // case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE); private int getEmojiForCategory(String category){
EditText edit=alert.findViewById(R.id.edit); return switch(category){
TextView supportingText=alert.findViewById(R.id.supporting_text); case "all" -> R.drawable.ic_category_all;
TextView label=alert.findViewById(R.id.label); case "academia" -> R.drawable.ic_category_academia;
TextView subtitle=alert.findViewById(R.id.subtitle); case "activism" -> R.drawable.ic_category_activism;
ImageButton clear=alert.findViewById(R.id.clear); case "food" -> R.drawable.ic_category_food;
clear.setVisibility(View.GONE); case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
if(TextUtils.isEmpty(domain)){ case "general" -> R.drawable.ic_category_general;
subtitle.setVisibility(View.GONE); case "journalism" -> R.drawable.ic_category_journalism;
}else{ case "lgbt" -> R.drawable.ic_category_lgbt;
subtitle.setText(getString(R.string.need_invite_to_join_server, domain)); case "regional" -> R.drawable.ic_category_regional;
} case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
Consumer<String> errorSetter=err->{ case "tech" -> R.drawable.ic_category_tech;
supportingText.setText(err); default -> R.drawable.ic_category_unknown;
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
supportingText.setTextColor(errorColor);
label.setTextColor(errorColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
}; };
}
next.setOnClickListener(_v->{ private int getTitleForCategory(String category){
Uri inviteLink=Uri.parse(edit.getText().toString()); return switch(category){
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){ case "all" -> R.string.category_all;
errorSetter.accept(getString(R.string.this_invite_is_invalid)); case "academia" -> R.string.category_academia;
return; case "activism" -> R.string.category_activism;
} case "food" -> R.string.category_food;
UiUtils.showProgressForAlertButton(next, true); case "furry" -> R.string.category_furry;
new CheckInviteLink(inviteLink.getPath()) case "games" -> R.string.category_games;
.setCallback(new Callback<>(){ case "general" -> R.string.category_general;
@Override case "journalism" -> R.string.category_journalism;
public void onSuccess(CheckInviteLink.Response result){ case "lgbt" -> R.string.category_lgbt;
if(getActivity()==null || !alert.isShowing()) case "regional" -> R.string.category_regional;
return; case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
String host=inviteLink.getHost(); case "tech" -> R.string.category_tech;
inviteCode=result.inviteCode; default -> 0;
inviteCodeHost=host; };
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
if(instance==null){
loadInstanceInfo(host, false, err->{
String errorStr;
if(err instanceof String str){
errorStr=str;
}else if(err instanceof Throwable x){
errorStr=x.getMessage();
}else if(err instanceof MastodonErrorResponse mer){
errorStr=mer.error;
}else{
errorStr=getString(R.string.error);
}
errorSetter.accept(errorStr);
UiUtils.showProgressForAlertButton(next, false);
});
}else{
proceedWithAuthOrSignup(instance);
}
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null || !alert.isShowing())
return;
UiUtils.showProgressForAlertButton(next, false);
if(error instanceof MastodonErrorResponse mer){
errorSetter.accept(switch(mer.httpStatus){
case 404 -> getString(R.string.this_invite_is_invalid);
case 401 -> getString(R.string.this_invite_has_expired);
default -> mer.error;
});
}
}
})
.execNoAuth(inviteLink.getHost());
});
next.setEnabled(false);
edit.addTextChangedListener(new SimpleTextWatcher(e->{
boolean wasEmpty=!next.isEnabled();
next.setEnabled(e.length()>0);
if(supportingText.length()>0){
supportingText.setText("");
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
supportingText.setTextColor(regularColor);
label.setTextColor(regularColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
}
if(wasEmpty!=(e.length()==0)){
int padEnd;
if(e.length()==0){
clear.setVisibility(View.GONE);
padEnd=V.dp(16);
}else{
clear.setVisibility(View.VISIBLE);
padEnd=V.dp(48);
}
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
}
}));
clear.setOnClickListener(_v->edit.setText(""));
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
edit.setText(clipText);
supportingText.setText(R.string.invite_link_pasted);
}
}
currentInviteLinkAlert=alert;
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
alert.show();
} }
@Override @Override
@@ -561,14 +444,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.clear(); filteredData.clear();
if(searchQueryMode){ if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){ if(!TextUtils.isEmpty(currentSearchQuery)){
String actualQuery;
if(currentSearchQuery.startsWith("https:")){
actualQuery=Uri.parse(currentSearchQuery).getHost();
}else{
actualQuery=currentSearchQuery;
}
for(CatalogInstance instance:data){ for(CatalogInstance instance:data){
if(instance.domain.contains(actualQuery)){ if(instance.domain.contains(currentSearchQuery)){
filteredData.add(instance); filteredData.add(instance);
} }
} }

View File

@@ -91,9 +91,6 @@ public class InstanceRulesFragment extends ToolbarFragment{
protected void onButtonClick(){ protected void onButtonClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this); Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
} }

View File

@@ -4,7 +4,6 @@ import android.app.ProgressDialog;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions; import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
@@ -13,38 +12,35 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion; import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel; import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder; import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID; private String accountID;
private View buttonBar; private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0; private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){ public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40); super(R.layout.fragment_onboarding_follow_suggestions, 40);
itemLayoutRes=R.layout.item_account_list_onboarding;
} }
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setRetainInstance(true); setRetainInstance(true);
setTitle(R.string.onboarding_recommendations_title); setTitle(R.string.popular_on_mastodon);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
loadData(); loadData();
} }
@@ -53,6 +49,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar); buttonBar=view.findViewById(R.id.button_bar);
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick)); view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed())); view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
@@ -61,7 +58,9 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
@Override @Override
protected void onUpdateToolbar(){ protected void onUpdateToolbar(){
super.onUpdateToolbar(); super.onUpdateToolbar();
getToolbar().setContentInsetsRelative(V.dp(56), 0); if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
} }
@Override @Override
@@ -70,7 +69,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<FollowSuggestion> result){ public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false); onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
} }
}) })
.exec(accountID); .exec(accountID);
@@ -81,19 +80,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets)); super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
} }
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView introText=new TextView(getActivity());
introText.setTextAppearance(R.style.m3_body_large);
introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
introText.setText(R.string.onboarding_recommendations_intro);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onFollowAllClick(View v){ private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty()) if(!loaded || relationships.isEmpty())
return; return;
@@ -169,6 +155,5 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
protected void onConfigureViewHolder(AccountViewHolder holder){ protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder); super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true); holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
} }
} }

View File

@@ -12,7 +12,6 @@ import android.view.WindowInsets;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView; import android.widget.ScrollView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -21,17 +20,12 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment; import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout; import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -42,7 +36,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
public class OnboardingProfileSetupFragment extends ToolbarFragment{ public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
private Button btn; private Button btn;
private View buttonBar; private View buttonBar;
private String accountID; private String accountID;
@@ -50,9 +44,9 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
private ScrollView scroller; private ScrollView scroller;
private EditText nameEdit, bioEdit; private EditText nameEdit, bioEdit;
private ImageView avaImage, coverImage; private ImageView avaImage, coverImage;
private Button addRow;
private ReorderableLinearLayout profileFieldsLayout;
private Uri avatarUri, coverUri; private Uri avatarUri, coverUri;
private LinearLayout scrollContent;
private CheckableListItem<Void> discoverableItem;
private static final int AVATAR_RESULT=348; private static final int AVATAR_RESULT=348;
private static final int COVER_RESULT=183; private static final int COVER_RESULT=183;
@@ -80,6 +74,8 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
bioEdit=view.findViewById(R.id.bio); bioEdit=view.findViewById(R.id.bio);
avaImage=view.findViewById(R.id.avatar); avaImage=view.findViewById(R.id.avatar);
coverImage=view.findViewById(R.id.header); coverImage=view.findViewById(R.id.header);
addRow=view.findViewById(R.id.add_row);
profileFieldsLayout=view.findViewById(R.id.profile_fields);
btn=view.findViewById(R.id.btn_next); btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick()); btn.setOnClickListener(v->onButtonClick());
@@ -91,20 +87,31 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
Account account=AccountSessionManager.getInstance().getAccount(accountID).self; Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
if(savedInstanceState==null){ if(savedInstanceState==null){
nameEdit.setText(account.displayName); nameEdit.setText(account.displayName);
makeFieldsRow();
}else{
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
for(int i=0;i<fieldTitles.size();i++){
View row=makeFieldsRow();
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
title.setText(fieldTitles.get(i));
content.setText(fieldValues.get(i));
}
if(fieldTitles.size()==4)
addRow.setVisibility(View.GONE);
} }
addRow.setOnClickListener(v->{
makeFieldsRow();
if(profileFieldsLayout.getChildCount()==4){
addRow.setVisibility(View.GONE);
}
});
profileFieldsLayout.setDragListener(this);
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT)); avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT)); coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
scrollContent=view.findViewById(R.id.scrollable_content);
discoverableItem=new CheckableListItem<>(R.string.make_profile_discoverable, 0, CheckableListItem.Style.SWITCH_SEPARATED, true, R.drawable.ic_campaign_24px, item->showDiscoverabilityAlert());
GenericListItemsAdapter<Void> fakeAdapter=new GenericListItemsAdapter<>(List.of(discoverableItem));
ListItemViewHolder<?> holder=fakeAdapter.onCreateViewHolder(scrollContent, fakeAdapter.getItemViewType(0));
fakeAdapter.bindViewHolder(holder, 0);
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
holder.itemView.setOnClickListener(v->holder.onClick());
scrollContent.addView(holder.itemView);
return view; return view;
} }
@@ -123,8 +130,17 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
} }
protected void onButtonClick(){ protected void onButtonClick(){
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, null) ArrayList<AccountField> fields=new ArrayList<>();
.setDiscoverableIndexable(discoverableItem.checked, discoverableItem.checked) for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
AccountField fld=new AccountField();
fld.name=title.getText().toString();
fld.value=content.getText().toString();
fields.add(fld);
}
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Account result){ public void onSuccess(Account result){
@@ -148,6 +164,39 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets)); super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
} }
private View makeFieldsRow(){
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
profileFieldsLayout.addView(view);
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
profileFieldsLayout.startDragging(view);
return true;
});
view.findViewById(R.id.delete).setOnClickListener(v->{
profileFieldsLayout.removeView(view);
if(addRow.getVisibility()==View.GONE)
addRow.setVisibility(View.VISIBLE);
});
return view;
}
@Override
public void onSwapItems(int oldIndex, int newIndex){}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
fieldTitles.add(title.getText().toString());
fieldValues.add(content.getText().toString());
}
outState.putStringArrayList("fieldTitles", fieldTitles);
outState.putStringArrayList("fieldValues", fieldValues);
}
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data){ public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode!=Activity.RESULT_OK) if(resultCode!=Activity.RESULT_OK)
@@ -167,12 +216,4 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
img.setForeground(null); img.setForeground(null);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size)); ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
} }
private void showDiscoverabilityAlert(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discoverability)
.setMessage(R.string.discoverability_help)
.setPositiveButton(R.string.ok, null)
.show();
}
} }

View File

@@ -1,15 +1,18 @@
package org.joinmastodon.android.fragments.onboarding; package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.graphics.Typeface;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable; import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -43,13 +46,10 @@ import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -58,6 +58,7 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment; import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{ public class SignupFragment extends ToolbarFragment{
@@ -78,7 +79,6 @@ public class SignupFragment extends ToolbarFragment{
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private HashSet<EditText> errorFields=new HashSet<>(); private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener; private ElevationOnScrollListener onScrollListener;
private Set<String> serverSupportedTimezones, serverSupportedLocales;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -87,8 +87,6 @@ public class SignupFragment extends ToolbarFragment{
instance=Parcels.unwrap(getArguments().getParcelable("instance")); instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken(); createAppAndGetToken();
setTitle(R.string.signup_title); setTitle(R.string.signup_title);
serverSupportedTimezones=Arrays.stream(getResources().getStringArray(R.array.server_supported_timezones)).collect(Collectors.toSet());
serverSupportedLocales=Arrays.stream(getResources().getStringArray(R.array.server_supported_locales)).collect(Collectors.toSet());
} }
@Nullable @Nullable
@@ -192,36 +190,7 @@ public class SignupFragment extends ToolbarFragment{
edit.setError(null); edit.setError(null);
} }
errorFields.clear(); errorFields.clear();
String locale=null; new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
String timezone=ZoneId.systemDefault().getId();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
LocaleList localeList=getResources().getConfiguration().getLocales();
for(int i=0;i<localeList.size();i++){
Locale l=localeList.get(i);
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
break;
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
break;
}
}
}else{
Locale l=getResources().getConfiguration().locale;
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
}
}
if(!serverSupportedTimezones.contains(timezone))
timezone=null;
String inviteCode=getArguments().getString("inviteCode");
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Token result){ public void onSuccess(Token result){
@@ -302,7 +271,7 @@ public class SignupFragment extends ToolbarFragment{
@Override @Override
public void tail(Node node, int depth){ public void tail(Node node, int depth){
if(node instanceof Element){ 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 LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }

View File

@@ -12,10 +12,12 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter; import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder; import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder; import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.V;
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
protected GenericListItemsAdapter<T> itemsAdapter; protected GenericListItemsAdapter<T> itemsAdapter;
@@ -43,7 +45,7 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
@Override @Override
protected RecyclerView.Adapter<?> getAdapter(){ protected RecyclerView.Adapter<?> getAdapter(){
return itemsAdapter=new GenericListItemsAdapter<T>(imgLoader, data); return itemsAdapter=new GenericListItemsAdapter<T>(data);
} }
@Override @Override
@@ -57,13 +59,12 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
return 0; return 0;
} }
protected void toggleCheckableItem(ListItem<?> item){ protected void toggleCheckableItem(CheckableListItem<T> item){
if(item instanceof CheckableListItem<?> checkable) item.toggle();
checkable.toggle();
rebindItem(item); rebindItem(item);
} }
protected void rebindItem(ListItem<?> item){ protected void rebindItem(ListItem<T> item){
if(list==null) if(list==null)
return; return;
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){ if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){

View File

@@ -73,7 +73,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick), durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick), wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick), contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, this::toggleCheckableItem) cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
)); ));
if(filter!=null){ if(filter!=null){
@@ -113,7 +113,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
return 1; return 1;
} }
private void onDurationClick(ListItem<Void> item_){ private void onDurationClick(){
int[] durationOptions={ int[] durationOptions={
1800, 1800,
3600, 3600,
@@ -182,21 +182,21 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
alert.setOnDismissListener(dialog->callback.accept(null)); alert.setOnDismissListener(dialog->callback.accept(null));
} }
private void onWordsClick(ListItem<Void> item){ private void onWordsClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new))); args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this); Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
} }
private void onContextClick(ListItem<Void> item){ private void onContextClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putSerializable("context", context); args.putSerializable("context", context);
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this); Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
} }
private void onDeleteClick(ListItem<Void> item_){ private void onDeleteClick(){
AlertDialog alert=new M3AlertDialogBuilder(getActivity()) AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(getString(R.string.settings_delete_filter_title, filter.title)) .setTitle(getString(R.string.settings_delete_filter_title, filter.title))
.setMessage(R.string.settings_delete_filter_confirmation) .setMessage(R.string.settings_delete_filter_confirmation)

View File

@@ -22,9 +22,10 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
setTitle(R.string.settings_filter_context); setTitle(R.string.settings_filter_context);
context=(EnumSet<FilterContext>) getArguments().getSerializable("context"); context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{ onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), this::toggleCheckableItem); CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
item.parentObject=c; item.parentObject=c;
item.isEnabled=true; item.isEnabled=true;
item.onClick=()->toggleCheckableItem(item);
return item; return item;
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
} }

View File

@@ -1,6 +1,11 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.IntEvaluator;
import android.animation.ObjectAnimator;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
@@ -22,7 +27,6 @@ import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.ActionModeHelper;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout; import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
@@ -33,6 +37,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@@ -55,7 +60,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
FilterKeyword word=Parcels.unwrap(p); FilterKeyword word=Parcels.unwrap(p);
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word); ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
item.isEnabled=true; item.isEnabled=true;
item.setOnClick(this::onWordClick); item.onClick=()->onWordClick(item);
return item; return item;
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
setHasOptionsMenu(true); setHasOptionsMenu(true);
@@ -109,7 +114,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.selectable_list, menu); inflater.inflate(R.menu.settings_filter_words, menu);
} }
@Override @Override
@@ -169,7 +174,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
w.keyword=input; w.keyword=input;
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w); ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
item.isEnabled=true; item.isEnabled=true;
item.setOnClick(this::onWordClick); item.onClick=()->onWordClick(item);
data.add(item); data.add(item);
itemsAdapter.notifyItemInserted(data.size()-1); itemsAdapter.notifyItemInserted(data.size()-1);
}else{ }else{
@@ -223,15 +228,29 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
return; return;
V.setVisibilityAnimated(fab, View.GONE); V.setVisibilityAnimated(fab, View.GONE);
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){ actionMode=getActivity().startActionMode(new ActionMode.Callback(){
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){ public boolean onCreateActionMode(ActionMode mode, Menu menu){
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
return true; return true;
} }
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){ public boolean onPrepareActionMode(ActionMode mode, Menu menu){
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu); mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
for(int i=0;i<menu.size();i++){
Drawable icon=menu.getItem(i).getIcon().mutate();
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
menu.getItem(i).setIcon(icon);
}
deleteItem=menu.findItem(R.id.delete); deleteItem=menu.findItem(R.id.delete);
return true; return true;
} }
@@ -247,6 +266,21 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
@Override @Override
public void onDestroyActionMode(ActionMode mode){ public void onDestroyActionMode(ActionMode mode){
leaveSelectionMode(true); leaveSelectionMode(true);
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
getActivity().getWindow().setStatusBarColor(0);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
} }
}); });
@@ -255,7 +289,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
ListItem<FilterKeyword> item=data.get(i); ListItem<FilterKeyword> item=data.get(i);
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null); CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
newItem.isEnabled=true; newItem.isEnabled=true;
newItem.setOnClick(this::onSelectionModeWordClick); newItem.onClick=()->onSelectionModeWordClick(newItem);
newItem.parentObject=item.parentObject; newItem.parentObject=item.parentObject;
if(selectAll) if(selectAll)
selectedItems.add(newItem); selectedItems.add(newItem);
@@ -279,7 +313,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
ListItem<FilterKeyword> item=data.get(i); ListItem<FilterKeyword> item=data.get(i);
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null); ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
newItem.isEnabled=true; newItem.isEnabled=true;
newItem.setOnClick(this::onWordClick); newItem.onClick=()->onWordClick(newItem);
newItem.parentObject=item.parentObject; newItem.parentObject=item.parentObject;
data.set(i, newItem); data.set(i, newItem);
} }

View File

@@ -1,9 +1,6 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.app.Activity; import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Gravity; import android.view.Gravity;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -16,7 +13,6 @@ import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List; import java.util.List;
@@ -36,10 +32,10 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
setTitle(getString(R.string.about_app, getString(R.string.app_name))); setTitle(getString(R.string.about_app, getString(R.string.app_name)));
AccountSession s=AccountSessionManager.get(accountID); AccountSession s=AccountSessionManager.get(accountID);
onDataLoaded(List.of( onDataLoaded(List.of(
new ListItem<>(R.string.settings_even_more, 0, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")), new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
new ListItem<>(R.string.settings_contribute, 0, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))), new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
new ListItem<>(R.string.settings_tos, 0, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")), new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true), new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick) mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
)); ));
@@ -61,20 +57,12 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline)); versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
versionInfo.setGravity(Gravity.CENTER); versionInfo.setGravity(Gravity.CENTER);
versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
versionInfo.setOnClickListener(v->{
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText("", BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")"));
if(Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2){
new Snackbar.Builder(getActivity())
.setText(R.string.app_version_copied)
.show();
}
});
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo)); adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
return adapter; return adapter;
} }
private void onClearMediaCacheClick(ListItem<?> item){ private void onClearMediaCacheClick(){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
Activity activity=getActivity(); Activity activity=getActivity();
ImageCache.getInstance(getActivity()).clear(); ImageCache.getInstance(getActivity()).clear();

View File

@@ -33,19 +33,19 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
onDataLoaded(List.of( onDataLoaded(List.of(
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick), languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, this::toggleCheckableItem), altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, ()->toggleCheckableItem(altTextItem)),
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, this::toggleCheckableItem), playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, ()->toggleCheckableItem(playGifsItem)),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, this::toggleCheckableItem), customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, ()->toggleCheckableItem(customTabsItem)),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, this::toggleCheckableItem), confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, ()->toggleCheckableItem(confirmUnfollowItem)),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, this::toggleCheckableItem), confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, ()->toggleCheckableItem(confirmBoostItem)),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, this::toggleCheckableItem) confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, ()->toggleCheckableItem(confirmDeleteItem))
)); ));
} }
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
private void onDefaultLanguageClick(ListItem<?> item){ private void onDefaultLanguageClick(){
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null); ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.default_post_language) .setTitle(R.string.default_post_language)

View File

@@ -1,9 +1,10 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.session.AccountActivationInfo; import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -27,8 +28,7 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick), new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick), selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick), resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick), new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick)
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
)); ));
if(!GithubSelfUpdater.needSelfUpdating()){ if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false; resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@@ -39,7 +39,7 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
private void onTestEmailConfirmClick(ListItem<?> item){ private void onTestEmailConfirmClick(){
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID); AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false; sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis()); sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
@@ -49,27 +49,22 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args); Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
} }
private void onForceSelfUpdateClick(ListItem<?> item){ private void onForceSelfUpdateClick(){
GithubSelfUpdater.forceUpdate=true; GithubSelfUpdater.forceUpdate=true;
GithubSelfUpdater.getInstance().maybeCheckForUpdates(); GithubSelfUpdater.getInstance().maybeCheckForUpdates();
restartUI(); restartUI();
} }
private void onResetUpdaterClick(ListItem<?> item){ private void onResetUpdaterClick(){
GithubSelfUpdater.getInstance().reset(); GithubSelfUpdater.getInstance().reset();
restartUI(); restartUI();
} }
private void onResetDiscoverBannersClick(ListItem<?> item){ private void onResetDiscoverBannersClick(){
DiscoverInfoBannerHelper.reset(); DiscoverInfoBannerHelper.reset();
restartUI(); restartUI();
} }
private void onResetPreReplySheetsClick(ListItem<?> item){
GlobalUserPreferences.resetPreReplySheets();
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
}
private void restartUI(){ private void restartUI(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);

View File

@@ -39,10 +39,10 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
AccountLocalPreferences lp=s.getLocalPreferences(); AccountLocalPreferences lp=s.getLocalPreferences();
onDataLoaded(List.of( onDataLoaded(List.of(
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick), themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, this::toggleCheckableItem), showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, ()->toggleCheckableItem(showCWsItem)),
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, this::toggleCheckableItem), hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, ()->toggleCheckableItem(hideSensitiveMediaItem)),
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, this::toggleCheckableItem), interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, ()->toggleCheckableItem(interactionCountsItem)),
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem) emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
)); ));
} }
@@ -80,7 +80,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
}; };
} }
private void onAppearanceClick(ListItem<?> item_){ private void onAppearanceClick(){
int selected=switch(GlobalUserPreferences.theme){ int selected=switch(GlobalUserPreferences.theme){
case LIGHT -> 0; case LIGHT -> 0;
case DARK -> 1; case DARK -> 1;

View File

@@ -67,14 +67,16 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
Nav.go(getActivity(), EditFilterFragment.class, args); Nav.go(getActivity(), EditFilterFragment.class, args);
} }
private void onAddFilterClick(ListItem<?> item){ private void onAddFilterClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), EditFilterFragment.class, args); Nav.go(getActivity(), EditFilterFragment.class, args);
} }
private ListItem<Filter> makeListItem(Filter f){ private ListItem<Filter> makeListItem(Filter f){
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), this::onFilterClick, f); ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
item.onClick=()->onFilterClick(item);
item.isEnabled=true;
return item; return item;
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@@ -16,7 +17,6 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -57,12 +57,11 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick), new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick), new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true), new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick),
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false) new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
)); ));
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){ if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true)); data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
} }
AccountSession session=AccountSessionManager.get(accountID); AccountSession session=AccountSessionManager.get(accountID);
@@ -123,45 +122,43 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
return args; return args;
} }
private void onBehaviorClick(ListItem<?> item_){ private void onBehaviorClick(){
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
} }
private void onDisplayClick(ListItem<?> item_){ private void onDisplayClick(){
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
} }
private void onPrivacyClick(ListItem<?> item_){ private void onPrivacyClick(){
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
} }
private void onFiltersClick(ListItem<?> item_){ private void onFiltersClick(){
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
} }
private void onNotificationsClick(ListItem<?> item_){ private void onNotificationsClick(){
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
} }
private void onServerClick(ListItem<?> item_){ private void onServerClick(){
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
} }
private void onAboutClick(ListItem<?> item_){ private void onAboutClick(){
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
} }
private void onManageAccountsClick(ListItem<?> item){ private void onLogOutClick(){
new AccountSwitcherSheet(getActivity(), null).setOnLoggedOutCallback(()->loggedOut=true).show();
}
private void onLogOutClick(ListItem<?> item_){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername())) .setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{ .setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
loggedOut=true; loggedOut=true;
((MainActivity)getActivity()).restartHomeFragment(); getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
})) }))
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show(); .show();

View File

@@ -55,14 +55,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
getPushSubscription(); getPushSubscription();
onDataLoaded(List.of( onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, i->onPauseNotificationsClick(false)), pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick), policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick),
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, this::toggleCheckableItem), mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, ()->toggleCheckableItem(mentionsItem)),
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, this::toggleCheckableItem), boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, ()->toggleCheckableItem(boostsItem)),
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, this::toggleCheckableItem), favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, ()->toggleCheckableItem(favoritesItem)),
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, this::toggleCheckableItem), followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, ()->toggleCheckableItem(followersItem)),
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, this::toggleCheckableItem) pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, ()->toggleCheckableItem(pollsItem))
)); ));
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem); typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
@@ -209,7 +209,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} }
private void onNotificationsPolicyClick(ListItem<?> item_){ private void onNotificationsPolicyClick(){
String[] items=Stream.of( String[] items=Stream.of(
R.string.notifications_policy_anyone, R.string.notifications_policy_anyone,
R.string.notifications_policy_followed, R.string.notifications_policy_followed,

View File

@@ -18,8 +18,8 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
setTitle(R.string.settings_privacy); setTitle(R.string.settings_privacy);
Account self=AccountSessionManager.get(accountID).self; Account self=AccountSessionManager.get(accountID).self;
onDataLoaded(List.of( onDataLoaded(List.of(
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, this::toggleCheckableItem), discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, ()->toggleCheckableItem(discoverableItem)),
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, this::toggleCheckableItem) indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, ()->toggleCheckableItem(indexableItem))
)); ));
if(self.source.indexable==null) if(self.source.indexable==null)
indexableItem.isEnabled=false; indexableItem.isEnabled=false;

Some files were not shown because too many files have changed in this diff Show More