Compare commits
218 Commits
2.1.4+fork
...
v2.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d5b84943d | ||
|
|
14fe992ca5 | ||
|
|
15232bddaf | ||
|
|
160ef25621 | ||
|
|
2afb8688a3 | ||
|
|
9d1af035ea | ||
|
|
fb7574d814 | ||
|
|
201a3cb9e3 | ||
|
|
cc735ee6a1 | ||
|
|
0165e14ea0 | ||
|
|
97d19605d5 | ||
|
|
bc490218f9 | ||
|
|
6dac05a21d | ||
|
|
fd3fff6322 | ||
|
|
edb64fff2e | ||
|
|
fe0e854e72 | ||
|
|
06c85fb203 | ||
|
|
69926c4ae1 | ||
|
|
ef44b0a412 | ||
|
|
8577ac1027 | ||
|
|
32da050106 | ||
|
|
526b74b3ef | ||
|
|
97ab328a9c | ||
|
|
21603eedcf | ||
|
|
7fbef273a1 | ||
|
|
9e19716504 | ||
|
|
b473642ab4 | ||
|
|
fba55f01a0 | ||
|
|
015e63ba66 | ||
|
|
d92e2407f3 | ||
|
|
a4f84fb8cd | ||
|
|
bfe88745ca | ||
|
|
0d334237ba | ||
|
|
fd5cff3fea | ||
|
|
af5b82e9fd | ||
|
|
d3561748c8 | ||
|
|
791a1d804b | ||
|
|
2442424e3b | ||
|
|
0ecedd2820 | ||
|
|
958d62ec0c | ||
|
|
400cfb2141 | ||
|
|
52b860dd8f | ||
|
|
4d57d8d576 | ||
|
|
9a098accd8 | ||
|
|
62f3b2522c | ||
|
|
9b48cd2037 | ||
|
|
69776d45d1 | ||
|
|
b8fb2660a4 | ||
|
|
941281298d | ||
|
|
8afc4511a6 | ||
|
|
f43ef325ae | ||
|
|
bbdc323204 | ||
|
|
8ed9fb6276 | ||
|
|
27cbb70352 | ||
|
|
f5b10b516c | ||
|
|
5580308968 | ||
|
|
901c70efc3 | ||
|
|
3d44e5d2cc | ||
|
|
33ea3da84d | ||
|
|
572901ec9d | ||
|
|
965239d215 | ||
|
|
ac1e5e991e | ||
|
|
e97203a6e3 | ||
|
|
66b7b127f9 | ||
|
|
b3f2987b14 | ||
|
|
c7426453a5 | ||
|
|
664d5cc4c3 | ||
|
|
a44e0e036a | ||
|
|
5d54c1bae4 | ||
|
|
2ef17ba051 | ||
|
|
f63daf3a4e | ||
|
|
52b079be2a | ||
|
|
efeca17106 | ||
|
|
6827166c1d | ||
|
|
04483e61e8 | ||
|
|
0ed858b99c | ||
|
|
9b3e153a4d | ||
|
|
e525aef3d9 | ||
|
|
22fe174922 | ||
|
|
f143da3913 | ||
|
|
7e9f41c74b | ||
|
|
a1474d0d29 | ||
|
|
0dce936ad3 | ||
|
|
6ebbbb4c6c | ||
|
|
dc6ddbd0ee | ||
|
|
86c81d6b53 | ||
|
|
451a92aa36 | ||
|
|
5c42e67e73 | ||
|
|
d20d36d964 | ||
|
|
1a8d46c71e | ||
|
|
91da10eca3 | ||
|
|
3bb0dcee53 | ||
|
|
d3fd4b200f | ||
|
|
fed96864e1 | ||
|
|
3b351bea27 | ||
|
|
254e01dca1 | ||
|
|
19158e1d48 | ||
|
|
bffb78fccf | ||
|
|
a3800592a2 | ||
|
|
22a498dfc9 | ||
|
|
9ea96e32bd | ||
|
|
68f51a123e | ||
|
|
43b1b63581 | ||
|
|
43b4a2c515 | ||
|
|
5b9cfdb689 | ||
|
|
b43cd7103a | ||
|
|
30e4f6e0f5 | ||
|
|
1d0ebf889b | ||
|
|
7c4f1da485 | ||
|
|
8163921014 | ||
|
|
993393dd96 | ||
|
|
82aed43934 | ||
|
|
fa65134c26 | ||
|
|
af4266c739 | ||
|
|
f72ea2e763 | ||
|
|
c5540270a3 | ||
|
|
201b72c9c8 | ||
|
|
26b99f5f68 | ||
|
|
d3dc774492 | ||
|
|
1f7155a932 | ||
|
|
02729fe02b | ||
|
|
498078b6e0 | ||
|
|
526f5e319b | ||
|
|
d419dba44a | ||
|
|
fd98159fce | ||
|
|
f5b98009dd | ||
|
|
cf0b66d852 | ||
|
|
197d0caf44 | ||
|
|
a4a082f76a | ||
|
|
84026afb92 | ||
|
|
4dea7d2a52 | ||
|
|
2df1b7dd61 | ||
|
|
89042113a5 | ||
|
|
48665ebcce | ||
|
|
2528d48010 | ||
|
|
5456d71979 | ||
|
|
e36aae3cf3 | ||
|
|
6d12e2dd72 | ||
|
|
f117249bb5 | ||
|
|
cf1d537367 | ||
|
|
517d13b400 | ||
|
|
103aaafff1 | ||
|
|
fae870c93a | ||
|
|
f8e00dcc80 | ||
|
|
5fdbb597bb | ||
|
|
d74b286a9d | ||
|
|
ecb3c521ff | ||
|
|
1d093ce928 | ||
|
|
46b711af2e | ||
|
|
772e6ddb5d | ||
|
|
f84e8443d2 | ||
|
|
250c18ebf1 | ||
|
|
e3d0f38b79 | ||
|
|
c512f97783 | ||
|
|
0594680775 | ||
|
|
f999881f59 | ||
|
|
4fe9192ac6 | ||
|
|
d936702fa9 | ||
|
|
74e284b0de | ||
|
|
4c42b72ed8 | ||
|
|
0e0046df65 | ||
|
|
c80d1d10c2 | ||
|
|
da97971011 | ||
|
|
700447dbe7 | ||
|
|
37e7b5ee93 | ||
|
|
1265afa93f | ||
|
|
1e09481b02 | ||
|
|
9996a5a05e | ||
|
|
f20aac7c81 | ||
|
|
98f7b0bacd | ||
|
|
3f6d3fb3a2 | ||
|
|
663b49c76b | ||
|
|
16e38f2541 | ||
|
|
842cc55e47 | ||
|
|
72db099e6f | ||
|
|
be130bc3a7 | ||
|
|
42253336e1 | ||
|
|
572631e1d7 | ||
|
|
723777a800 | ||
|
|
b825d534c1 | ||
|
|
b9749620a8 | ||
|
|
70ea9989aa | ||
|
|
b3ec9c981c | ||
|
|
bf72085abb | ||
|
|
64dd416b59 | ||
|
|
ab2a920455 | ||
|
|
7580446d60 | ||
|
|
ade18ac6fc | ||
|
|
005c851d72 | ||
|
|
0f1d46c765 | ||
|
|
21fbb07b1d | ||
|
|
dff2217e80 | ||
|
|
22aac3d943 | ||
|
|
53afc120f3 | ||
|
|
a75ce70615 | ||
|
|
4a3b948760 | ||
|
|
f81283c892 | ||
|
|
7eae879037 | ||
|
|
1b0ce5d893 | ||
|
|
c17745368d | ||
|
|
e78b518654 | ||
|
|
55a8634be2 | ||
|
|
ac891eea53 | ||
|
|
74fa2a3081 | ||
|
|
6c1c5b7759 | ||
|
|
1f4152b588 | ||
|
|
70386ea1b2 | ||
|
|
cbce90c461 | ||
|
|
74ae3bf706 | ||
|
|
1feccdc26d | ||
|
|
c38c2a425b | ||
|
|
f43352b790 | ||
|
|
c5b52b2781 | ||
|
|
b91840fb95 | ||
|
|
fc10fbffb0 | ||
|
|
e40841c128 | ||
|
|
98a02e874b | ||
|
|
b06df8c3d0 |
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# 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']
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 70
|
||||
versionName "2.1.4"
|
||||
versionCode 76
|
||||
versionName "2.2.1"
|
||||
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", "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"
|
||||
}
|
||||
@@ -76,7 +76,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.13'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.15'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -42,40 +42,7 @@ public class MainActivity extends FragmentStackActivity{
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState==null){
|
||||
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();
|
||||
}
|
||||
}
|
||||
restartHomeFragment();
|
||||
}
|
||||
|
||||
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
|
||||
@@ -200,4 +167,41 @@ public class MainActivity extends FragmentStackActivity{
|
||||
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")){
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,31 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
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.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
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.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -42,6 +53,7 @@ public class CacheController{
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@@ -300,6 +312,99 @@ public class CacheController{
|
||||
}, 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{
|
||||
|
||||
public DatabaseHelper(){
|
||||
|
||||
@@ -92,15 +92,15 @@ public class MastodonAPIController{
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||
|
||||
call.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
||||
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -109,10 +109,10 @@ public class MastodonAPIController{
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(call.isCanceled())
|
||||
if(req.canceled)
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
||||
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ public class MastodonAPIController{
|
||||
try{
|
||||
if(BuildConfig.DEBUG){
|
||||
JsonElement respJson=JsonParser.parseReader(reader);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else if(req.respClass!=null)
|
||||
@@ -140,7 +140,7 @@ public class MastodonAPIController{
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
@@ -149,19 +149,19 @@ public class MastodonAPIController{
|
||||
req.validateAndPostprocessResponse(respObj, response);
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
||||
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||
|
||||
req.onSuccess(respObj);
|
||||
}else{
|
||||
try{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||
if(error.has("details")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
@@ -196,7 +196,7 @@ public class MastodonAPIController{
|
||||
});
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage(), 0, x);
|
||||
}
|
||||
}, 0);
|
||||
@@ -209,4 +209,8 @@ public class MastodonAPIController{
|
||||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static String logTag(AccountSession session){
|
||||
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
if(requestBody instanceof RequestBody rb)
|
||||
return rb;
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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<>(){});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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+"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("remote", "true");
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(!TextUtils.isEmpty(minID))
|
||||
addQueryParameter("min_id", minID);
|
||||
if(!TextUtils.isEmpty(sinceID))
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -32,6 +33,7 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -66,6 +68,7 @@ public class AccountSession{
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -265,4 +268,12 @@ public class AccountSession{
|
||||
public void updateAccountInfo(){
|
||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||
}
|
||||
|
||||
public boolean isNotificationsMentionsOnly(){
|
||||
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
|
||||
}
|
||||
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ public class AccountSessionManager{
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,17 +19,14 @@ import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@@ -49,7 +46,6 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
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.EditStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
@@ -57,7 +53,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
@@ -340,7 +336,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
@@ -1016,8 +1012,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
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
|
||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||
description = sanitizeMediaDescription(description);
|
||||
return mediaViewController.addMediaAttachment(uri, description);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.tags.GetTag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||
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.Status;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
@@ -47,6 +49,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
private MenuItem followMenuItem;
|
||||
private boolean followRequestRunning;
|
||||
private boolean toolbarContentVisible;
|
||||
private String maxID;
|
||||
|
||||
public HashtagTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -67,10 +70,13 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : getMaxID(), null, count)
|
||||
currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : maxID, null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
})
|
||||
@@ -176,7 +182,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void updateHeader(){
|
||||
if(hashtag==null)
|
||||
if(hashtag==null || getActivity()==null)
|
||||
return;
|
||||
|
||||
if(hashtag.history!=null && !hashtag.history.isEmpty()){
|
||||
|
||||
@@ -5,40 +5,50 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
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.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.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
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.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -52,44 +62,141 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
|
||||
private ImageButton fab;
|
||||
private ImageView toolbarLogo;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private LinearLayout listsDropdown;
|
||||
private FixedAspectRatioImageView listsDropdownArrow;
|
||||
private TextView listsDropdownText;
|
||||
private Button newPostsBtn;
|
||||
private View newPostsBtnWrap;
|
||||
private boolean newPostsBtnShown;
|
||||
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 lastSavedMarkerID;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
setListLayoutId(R.layout.fragment_timeline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity 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);
|
||||
loadData();
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
lists=result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
});
|
||||
switch(listMode){
|
||||
case FOLLOWING -> {
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null || listMode!=ListMode.FOLLOWING)
|
||||
return;
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
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;
|
||||
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;
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,6 +204,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
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();
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
@@ -116,13 +236,26 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
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
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
int id=item.getItemId();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -147,7 +280,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!data.isEmpty()){
|
||||
if(!data.isEmpty() && listMode==ListMode.FOLLOWING){
|
||||
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
|
||||
if(!topPostID.equals(lastSavedMarkerID)){
|
||||
lastSavedMarkerID=topPostID;
|
||||
@@ -183,8 +316,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
// 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.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(null, null, 20, sinceID, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
@@ -199,11 +332,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +347,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -225,10 +359,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
V.setVisibilityAnimated(item.text, View.GONE);
|
||||
GapStatusDisplayItem gap=item.getItem();
|
||||
dataLoading=true;
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
if(getActivity()==null)
|
||||
@@ -242,7 +377,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
Status gapStatus=getStatusByID(gap.parentID);
|
||||
if(gapStatus!=null){
|
||||
gapStatus.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
}
|
||||
}else{
|
||||
Set<String> idsBelowGap=new HashSet<>();
|
||||
@@ -254,7 +390,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else if(s.id.equals(gap.parentID)){
|
||||
belowGap=true;
|
||||
s.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
}else{
|
||||
gapPostIndex++;
|
||||
}
|
||||
@@ -270,7 +407,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else{
|
||||
result=result.subList(0, endIndex);
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
@@ -287,7 +425,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||
}
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,9 +443,17 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
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
|
||||
@@ -320,42 +467,41 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void updateToolbarLogo(){
|
||||
toolbarLogo=new ImageView(getActivity());
|
||||
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
toolbarLogo.setImageResource(R.drawable.logo);
|
||||
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
|
||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||
toolbarShowNewPostsBtn.setTextColor(0xffffffff);
|
||||
toolbarShowNewPostsBtn.setStateListAnimator(null);
|
||||
toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
|
||||
toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
|
||||
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
toolbarLogo.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
listsDropdown=new LinearLayout(getActivity());
|
||||
listsDropdown.setOnClickListener(this::onListsDropdownClick);
|
||||
listsDropdown.setBackgroundResource(R.drawable.bg_button_m3_text);
|
||||
listsDropdown.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName("android.widget.Spinner");
|
||||
}
|
||||
});
|
||||
listsDropdownArrow=new FixedAspectRatioImageView(getActivity());
|
||||
listsDropdownArrow.setUseHeight(true);
|
||||
listsDropdownArrow.setImageResource(R.drawable.ic_arrow_drop_down_24px);
|
||||
listsDropdownArrow.setScaleType(ImageView.ScaleType.CENTER);
|
||||
listsDropdownArrow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
listsDropdown.addView(listsDropdownArrow, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
listsDropdownText=new TextView(getActivity());
|
||||
listsDropdownText.setTextAppearance(R.style.action_bar_title);
|
||||
listsDropdownText.setSingleLine();
|
||||
listsDropdownText.setEllipsize(TextUtils.TruncateAt.END);
|
||||
listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0);
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
listsDropdownArrow.setImageTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.setBackgroundTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.addView(listsDropdownText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||
FrameLayout.LayoutParams ddlp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START);
|
||||
ddlp.topMargin=ddlp.bottomMargin=V.dp(8);
|
||||
logoWrap.addView(listsDropdown, ddlp);
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
toolbar.setContentInsetsRelative(V.dp(16), 0);
|
||||
}
|
||||
|
||||
private void showNewPostsButton(){
|
||||
@@ -365,20 +511,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
newPostsBtnWrap.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, 1f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3));
|
||||
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_decelerate));
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@@ -393,20 +538,19 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, .9f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, .9f),
|
||||
ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, V.dp(-56))
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3));
|
||||
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_accelerate));
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
newPostsBtnWrap.setVisibility(View.GONE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@@ -421,6 +565,20 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
@@ -443,4 +601,67 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
@@ -35,8 +36,15 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(wantsElevationOnScrollEffect()){
|
||||
FragmentRootLinearLayout rootView;
|
||||
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){
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
|
||||
|
||||
@@ -44,7 +44,7 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions=true;
|
||||
private boolean onlyMentions;
|
||||
private String maxID;
|
||||
private View tabBar;
|
||||
private View mentionsTab, allTab;
|
||||
@@ -58,9 +58,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_notifications);
|
||||
E.register(this);
|
||||
if(savedInstanceState!=null){
|
||||
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
|
||||
}
|
||||
onlyMentions=AccountSessionManager.get(accountID).isNotificationsMentionsOnly();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@@ -132,13 +130,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,12 +215,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
return views;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("onlyMentions", onlyMentions);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n:data){
|
||||
if(n.id.equals(id))
|
||||
@@ -291,8 +279,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
allTab.setSelected(!onlyMentions);
|
||||
maxID=null;
|
||||
showProgress();
|
||||
loadData(0, 20);
|
||||
refreshing=true;
|
||||
reloadingFromCache=true;
|
||||
loadData(0, 20);
|
||||
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -312,7 +302,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -330,7 +319,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,10 +336,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMarkAllReadButton(){
|
||||
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendItems(List<Notification> items){
|
||||
super.onAppendItems(items);
|
||||
@@ -364,4 +348,20 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +301,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
|
||||
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();
|
||||
}
|
||||
UiUtils.maybeShowTextCopiedToast(getActivity());
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -609,6 +607,7 @@ 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()));
|
||||
else
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -662,6 +661,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
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;
|
||||
}
|
||||
@@ -850,6 +854,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
aboutFragment.enterEditMode(account.source.fields);
|
||||
refreshLayout.setEnabled(false);
|
||||
editDirty=false;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
}
|
||||
|
||||
private void exitEditMode(){
|
||||
@@ -892,6 +897,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
refreshLayout.setEnabled(true);
|
||||
|
||||
bindHeaderView();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
|
||||
private void saveAndExitEditMode(){
|
||||
|
||||
@@ -47,13 +47,12 @@ public class SplashFragment extends AppKitFragment{
|
||||
private ProgressBarButton defaultServerButton;
|
||||
private ProgressBar defaultServerProgress;
|
||||
private String chosenDefaultServer=DEFAULT_SERVER;
|
||||
private boolean loadingDefaultServer;
|
||||
private boolean loadingDefaultServer, loadedDefaultServer;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
|
||||
loadAndChooseDefaultServer();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -101,6 +100,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
});
|
||||
}
|
||||
});
|
||||
if(!loadedDefaultServer && !loadingDefaultServer)
|
||||
loadAndChooseDefaultServer();
|
||||
|
||||
return contentView;
|
||||
}
|
||||
@@ -198,6 +199,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
|
||||
private void loadAndChooseDefaultServer(){
|
||||
loadingDefaultServer=true;
|
||||
defaultServerButton.setTextVisible(false);
|
||||
defaultServerProgress.setVisibility(View.VISIBLE);
|
||||
new GetCatalogDefaultInstances()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -239,6 +242,7 @@ public class SplashFragment extends AppKitFragment{
|
||||
private void setChosenDefaultServer(String domain){
|
||||
chosenDefaultServer=domain;
|
||||
loadingDefaultServer=false;
|
||||
loadedDefaultServer=true;
|
||||
if(defaultServerButton!=null && getActivity()!=null){
|
||||
defaultServerButton.setTextVisible(true);
|
||||
defaultServerProgress.setVisibility(View.GONE);
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
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.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
@@ -13,13 +15,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
protected String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@@ -28,12 +31,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
|
||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -51,13 +53,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||
}
|
||||
})
|
||||
.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
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
@@ -0,0 +1,37 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@@ -122,20 +124,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
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
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
@@ -150,6 +141,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
protected void onBindViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
@@ -167,6 +159,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,4 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
||||
status=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasSubtitle(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private DiscoverNewsFragment newsFragment;
|
||||
private DiscoverAccountsFragment accountsFragment;
|
||||
private SearchFragment searchFragment;
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
|
||||
private String accountID;
|
||||
private String currentQuery;
|
||||
@@ -71,15 +70,14 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[5];
|
||||
tabViews=new FrameLayout[4];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.discover_posts;
|
||||
case 1 -> R.id.discover_hashtags;
|
||||
case 2 -> R.id.discover_news;
|
||||
case 3 -> R.id.discover_local_timeline;
|
||||
case 4 -> R.id.discover_users;
|
||||
case 3 -> R.id.discover_users;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -122,12 +120,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
accountsFragment=new DiscoverAccountsFragment();
|
||||
accountsFragment.setArguments(args);
|
||||
|
||||
localTimelineFragment=new LocalTimelineFragment();
|
||||
localTimelineFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.discover_posts, postsFragment)
|
||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||
.add(R.id.discover_news, newsFragment)
|
||||
.add(R.id.discover_users, accountsFragment)
|
||||
@@ -141,8 +135,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 0 -> R.string.posts;
|
||||
case 1 -> R.string.hashtags;
|
||||
case 2 -> R.string.news;
|
||||
case 3 -> R.string.local_timeline;
|
||||
case 4 -> R.string.for_you;
|
||||
case 3 -> R.string.for_you;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
}
|
||||
@@ -245,8 +238,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> hashtagsFragment;
|
||||
case 2 -> newsFragment;
|
||||
case 3 -> localTimelineFragment;
|
||||
case 4 -> accountsFragment;
|
||||
case 3 -> accountsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.os.Bundle;
|
||||
|
||||
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.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
|
||||
@@ -15,6 +17,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class DiscoverPostsFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private int realOffset=0;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -24,10 +27,12 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetTrendingStatuses(offset, count)
|
||||
currentRequest=new GetTrendingStatuses(offset==0 ? 0 : realOffset, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
realOffset+=result.size();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ 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 SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
private String currentQuery;
|
||||
@@ -137,7 +138,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
}*/
|
||||
int offset=_offset;
|
||||
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
|
||||
.setCallback(new Callback<>(){
|
||||
.setCallback(new SimpleCallback<SearchResults>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
ArrayList<SearchResult> results=new ArrayList<>();
|
||||
@@ -158,16 +159,10 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
}
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
unfilteredResults=results;
|
||||
boolean wasRefreshing=refreshing;
|
||||
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity a=getActivity();
|
||||
if(a==null)
|
||||
return;
|
||||
error.showToast(a);
|
||||
if(wasRefreshing)
|
||||
list.scrollToPosition(0);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -112,7 +112,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
onDataLoaded(results.stream().map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
return vm;
|
||||
}).collect(Collectors.toList()), false);
|
||||
@@ -129,7 +129,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
.map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
return vm;
|
||||
})
|
||||
@@ -389,18 +389,18 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
deliverResult(currentQuery, null);
|
||||
}
|
||||
|
||||
private void onOpenURLClick(){
|
||||
private void onOpenURLClick(ListItem<?> item_){
|
||||
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
|
||||
}
|
||||
|
||||
private void onGoToHashtagClick(){
|
||||
private void onGoToHashtagClick(ListItem<?> item_){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(q.startsWith("#"))
|
||||
q=q.substring(1);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
|
||||
}
|
||||
|
||||
private void onGoToAccountClick(){
|
||||
private void onGoToAccountClick(ListItem<?> item_){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(!q.startsWith("@")){
|
||||
q="@"+q;
|
||||
@@ -411,11 +411,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
|
||||
}
|
||||
|
||||
private void onGoToStatusSearchClick(){
|
||||
private void onGoToStatusSearchClick(ListItem<?> item_){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
|
||||
}
|
||||
|
||||
private void onGoToAccountSearchClick(){
|
||||
private void onGoToAccountSearchClick(ListItem<?> item_){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,9 +165,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
private void tryGetAccount(){
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(accountID)==null){
|
||||
uiHandler.removeCallbacks(pollRunnable);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
((MainActivity)getActivity()).restartHomeFragment();
|
||||
return;
|
||||
}
|
||||
currentRequest=new GetOwnAccount()
|
||||
|
||||
@@ -12,12 +12,10 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
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.SimpleListItemViewHolder;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
|
||||
protected GenericListItemsAdapter<T> itemsAdapter;
|
||||
@@ -45,7 +43,7 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(imgLoader, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,12 +57,13 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
protected void toggleCheckableItem(ListItem<?> item){
|
||||
if(item instanceof CheckableListItem<?> checkable)
|
||||
checkable.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
protected void rebindItem(ListItem<?> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
|
||||
@@ -73,7 +73,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
|
||||
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
|
||||
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, ()->toggleCheckableItem(cwItem))
|
||||
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)
|
||||
));
|
||||
|
||||
if(filter!=null){
|
||||
@@ -113,7 +113,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void onDurationClick(){
|
||||
private void onDurationClick(ListItem<Void> item_){
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
@@ -182,21 +182,21 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
alert.setOnDismissListener(dialog->callback.accept(null));
|
||||
}
|
||||
|
||||
private void onWordsClick(){
|
||||
private void onWordsClick(ListItem<Void> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
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);
|
||||
}
|
||||
|
||||
private void onContextClick(){
|
||||
private void onContextClick(ListItem<Void> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("context", context);
|
||||
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
|
||||
}
|
||||
|
||||
private void onDeleteClick(){
|
||||
private void onDeleteClick(ListItem<Void> item_){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
|
||||
.setMessage(R.string.settings_delete_filter_confirmation)
|
||||
|
||||
@@ -22,10 +22,9 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
|
||||
setTitle(R.string.settings_filter_context);
|
||||
context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
|
||||
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), this::toggleCheckableItem);
|
||||
item.parentObject=c;
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->toggleCheckableItem(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
@@ -27,6 +22,7 @@ import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.ActionModeHelper;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
@@ -37,7 +33,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@@ -60,7 +55,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
FilterKeyword word=Parcels.unwrap(p);
|
||||
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
item.setOnClick(this::onWordClick);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
setHasOptionsMenu(true);
|
||||
@@ -114,7 +109,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_filter_words, menu);
|
||||
inflater.inflate(R.menu.selectable_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,7 +169,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
w.keyword=input;
|
||||
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
item.setOnClick(this::onWordClick);
|
||||
data.add(item);
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}else{
|
||||
@@ -228,29 +223,15 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
return;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
|
||||
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
|
||||
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu 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);
|
||||
return true;
|
||||
}
|
||||
@@ -266,21 +247,6 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,7 +255,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onSelectionModeWordClick(newItem);
|
||||
newItem.setOnClick(this::onSelectionModeWordClick);
|
||||
newItem.parentObject=item.parentObject;
|
||||
if(selectAll)
|
||||
selectedItems.add(newItem);
|
||||
@@ -313,7 +279,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onWordClick(newItem);
|
||||
newItem.setOnClick(this::onWordClick);
|
||||
newItem.parentObject=item.parentObject;
|
||||
data.set(i, newItem);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
setTitle(getString(R.string.about_app, getString(R.string.app_name)));
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
|
||||
new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
new ListItem<>(R.string.settings_even_more, 0, i->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_tos, 0, i->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),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
|
||||
));
|
||||
|
||||
@@ -62,7 +62,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onClearMediaCacheClick(){
|
||||
private void onClearMediaCacheClick(ListItem<?> item){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
|
||||
@@ -33,19 +33,19 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
|
||||
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),
|
||||
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, ()->toggleCheckableItem(playGifsItem)),
|
||||
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, ()->toggleCheckableItem(confirmUnfollowItem)),
|
||||
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, ()->toggleCheckableItem(confirmDeleteItem))
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, this::toggleCheckableItem),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, this::toggleCheckableItem),
|
||||
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, this::toggleCheckableItem),
|
||||
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, this::toggleCheckableItem),
|
||||
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, this::toggleCheckableItem),
|
||||
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, this::toggleCheckableItem)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(){
|
||||
private void onDefaultLanguageClick(ListItem<?> item){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
|
||||
@@ -39,7 +39,7 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onTestEmailConfirmClick(){
|
||||
private void onTestEmailConfirmClick(ListItem<?> item){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
@@ -49,18 +49,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}
|
||||
|
||||
private void onForceSelfUpdateClick(){
|
||||
private void onForceSelfUpdateClick(ListItem<?> item){
|
||||
GithubSelfUpdater.forceUpdate=true;
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetUpdaterClick(){
|
||||
private void onResetUpdaterClick(ListItem<?> item){
|
||||
GithubSelfUpdater.getInstance().reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetDiscoverBannersClick(){
|
||||
private void onResetDiscoverBannersClick(ListItem<?> item){
|
||||
DiscoverInfoBannerHelper.reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
onDataLoaded(List.of(
|
||||
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, ()->toggleCheckableItem(showCWsItem)),
|
||||
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, ()->toggleCheckableItem(interactionCountsItem)),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_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, this::toggleCheckableItem),
|
||||
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, this::toggleCheckableItem),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
};
|
||||
}
|
||||
|
||||
private void onAppearanceClick(){
|
||||
private void onAppearanceClick(ListItem<?> item_){
|
||||
int selected=switch(GlobalUserPreferences.theme){
|
||||
case LIGHT -> 0;
|
||||
case DARK -> 1;
|
||||
|
||||
@@ -67,16 +67,14 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddFilterClick(){
|
||||
private void onAddFilterClick(ListItem<?> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private ListItem<Filter> makeListItem(Filter 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;
|
||||
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), this::onFilterClick, f);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -57,11 +58,12 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
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<>(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)
|
||||
));
|
||||
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
@@ -122,43 +124,45 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onBehaviorClick(){
|
||||
private void onBehaviorClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDisplayClick(){
|
||||
private void onDisplayClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onPrivacyClick(){
|
||||
private void onPrivacyClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(){
|
||||
private void onFiltersClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(){
|
||||
private void onNotificationsClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onServerClick(){
|
||||
private void onServerClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(){
|
||||
private void onAboutClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onLogOutClick(){
|
||||
private void onManageAccountsClick(ListItem<?> item){
|
||||
new AccountSwitcherSheet(getActivity(), null).show();
|
||||
}
|
||||
|
||||
private void onLogOutClick(ListItem<?> item_){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
((MainActivity)getActivity()).restartHomeFragment();
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
@@ -55,14 +55,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
getPushSubscription();
|
||||
|
||||
onDataLoaded(List.of(
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, i->onPauseNotificationsClick(false)),
|
||||
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, ()->toggleCheckableItem(mentionsItem)),
|
||||
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, ()->toggleCheckableItem(favoritesItem)),
|
||||
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, ()->toggleCheckableItem(pollsItem))
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, this::toggleCheckableItem),
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, this::toggleCheckableItem),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, this::toggleCheckableItem),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, this::toggleCheckableItem),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, this::toggleCheckableItem)
|
||||
));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyClick(){
|
||||
private void onNotificationsPolicyClick(ListItem<?> item_){
|
||||
String[] items=Stream.of(
|
||||
R.string.notifications_policy_anyone,
|
||||
R.string.notifications_policy_followed,
|
||||
|
||||
@@ -18,8 +18,8 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
|
||||
setTitle(R.string.settings_privacy);
|
||||
Account self=AccountSessionManager.get(accountID).self;
|
||||
onDataLoaded(List.of(
|
||||
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, ()->toggleCheckableItem(indexableItem))
|
||||
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_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, this::toggleCheckableItem)
|
||||
));
|
||||
if(self.source.indexable==null)
|
||||
indexableItem.isEnabled=false;
|
||||
|
||||
@@ -139,7 +139,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
if(!TextUtils.isEmpty(instance.email)){
|
||||
needDivider=true;
|
||||
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, ()->{});
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, i->{});
|
||||
holder.bind(item);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->openAdminEmail());
|
||||
|
||||
@@ -45,26 +45,34 @@ public class Attachment extends BaseModel{
|
||||
|
||||
public int getWidth(){
|
||||
if(meta==null)
|
||||
return 0;
|
||||
return 1920;
|
||||
if(meta.width>0)
|
||||
return meta.width;
|
||||
if(meta.original!=null && meta.original.width>0)
|
||||
return meta.original.width;
|
||||
if(meta.small!=null && meta.small.width>0)
|
||||
return meta.small.width;
|
||||
return 0;
|
||||
return 1920;
|
||||
}
|
||||
|
||||
public int getHeight(){
|
||||
if(meta==null)
|
||||
return 0;
|
||||
return 1080;
|
||||
if(meta.height>0)
|
||||
return meta.height;
|
||||
if(meta.original!=null && meta.original.height>0)
|
||||
return meta.original.height;
|
||||
if(meta.small!=null && meta.small.height>0)
|
||||
return meta.small.height;
|
||||
return 0;
|
||||
return 1080;
|
||||
}
|
||||
|
||||
public boolean hasKnownDimensions(){
|
||||
return meta!=null && (
|
||||
(meta.height>0 && meta.width>0)
|
||||
|| (meta.original!=null && meta.original.height>0 && meta.original.width>0)
|
||||
|| (meta.small!=null && meta.small.height>0 && meta.small.width>0)
|
||||
);
|
||||
}
|
||||
|
||||
public double getDuration(){
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
// Called like this to avoid conflict with java.util.List
|
||||
@AllFieldsAreRequired
|
||||
@Parcel
|
||||
public class FollowList extends BaseModel{
|
||||
public String id;
|
||||
public String title;
|
||||
public RepliesPolicy repliesPolicy=RepliesPolicy.LIST;
|
||||
public boolean exclusive;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "FollowList{"+
|
||||
"id='"+id+'\''+
|
||||
", title='"+title+'\''+
|
||||
", repliesPolicy="+repliesPolicy+
|
||||
", exclusive="+exclusive+
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
if(repliesPolicy==null)
|
||||
repliesPolicy=RepliesPolicy.LIST;
|
||||
super.postprocess();
|
||||
}
|
||||
|
||||
public enum RepliesPolicy{
|
||||
@SerializedName("followed")
|
||||
FOLLOWED,
|
||||
@SerializedName("list")
|
||||
LIST,
|
||||
@SerializedName("none")
|
||||
NONE
|
||||
}
|
||||
}
|
||||
@@ -45,4 +45,8 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
|
||||
public int hashCode(){
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
public int getWeekPosts(){
|
||||
return history.stream().mapToInt(h->h.uses).sum();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,10 @@ public class HeaderPaginationList<T> extends ArrayList<T>{
|
||||
public HeaderPaginationList(@NonNull Collection<? extends T> c){
|
||||
super(c);
|
||||
}
|
||||
|
||||
public String getNextPageMaxID(){
|
||||
if(nextPageUri==null)
|
||||
return null;
|
||||
return nextPageUri.getQueryParameter("max_id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public StatusPrivacy visibility;
|
||||
public boolean sensitive;
|
||||
@RequiredField
|
||||
public String spoilerText;
|
||||
public String spoilerText="";
|
||||
@RequiredField
|
||||
public List<Attachment> mediaAttachments;
|
||||
public Application application;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class AvatarPileListItem<T> extends ListItem<T>{
|
||||
public List<ImageLoaderRequest> avatars;
|
||||
|
||||
public AvatarPileListItem(String title, String subtitle, List<ImageLoaderRequest> avatars, int iconRes, Consumer<AvatarPileListItem<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.avatars=avatars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_avatar_pile;
|
||||
}
|
||||
}
|
||||
@@ -9,42 +9,42 @@ public class CheckableListItem<T> extends ListItem<T>{
|
||||
public boolean checked;
|
||||
public Consumer<Boolean> checkedChangeListener;
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, onClick, parentObject, 0, dividerAfter);
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
|
||||
this(title, subtitle, style, checked, 0, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick, T parentObject){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, 0, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick, boolean dividerAfter){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
|
||||
this(titleRes, subtitleRes, style, checked, iconRes, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick, boolean dividerAfter){
|
||||
super(titleRes, subtitleRes, iconRes, onClick, 0, dividerAfter);
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
|
||||
super(titleRes, subtitleRes, iconRes, (Consumer<ListItem<T>>)(Object)onClick, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@@ -16,11 +18,11 @@ public class ListItem<T>{
|
||||
public int iconRes;
|
||||
public int colorOverrideAttr;
|
||||
public boolean dividerAfter;
|
||||
public Runnable onClick;
|
||||
private Consumer<ListItem<T>> onClick;
|
||||
public boolean isEnabled=true;
|
||||
public T parentObject;
|
||||
|
||||
public ListItem(String title, String subtitle, int iconRes, Runnable onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(String title, String subtitle, int iconRes, Consumer<ListItem<T>> onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.iconRes=iconRes;
|
||||
@@ -32,41 +34,41 @@ public class ListItem<T>{
|
||||
isEnabled=false;
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick){
|
||||
public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick){
|
||||
this(title, subtitle, 0, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick, T parentObject){
|
||||
public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, 0, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick){
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
|
||||
this(title, subtitle, iconRes, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, T parentObject){
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, iconRes, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick){
|
||||
this(null, null, 0, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
|
||||
this(null, null, iconRes, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
@@ -75,4 +77,13 @@ public class ListItem<T>{
|
||||
public int getItemViewType(){
|
||||
return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted;
|
||||
}
|
||||
|
||||
public void performClick(){
|
||||
if(onClick!=null)
|
||||
onClick.accept(this);
|
||||
}
|
||||
|
||||
public <I extends ListItem<T>> void setOnClick(Consumer<I> onClick){
|
||||
this.onClick=(Consumer<ListItem<T>>) onClick;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ListItemWithOptionsMenu<T> extends ListItem<T>{
|
||||
public OptionsMenuListener<T> listener;
|
||||
|
||||
public ListItemWithOptionsMenu(String title, String subtitle, OptionsMenuListener<T> listener, int iconRes, Consumer<ListItemWithOptionsMenu<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_options;
|
||||
}
|
||||
|
||||
public void performConfigureMenu(Menu menu){
|
||||
listener.onConfigureListItemOptionsMenu(this, menu);
|
||||
}
|
||||
|
||||
public void performItemSelected(MenuItem item){
|
||||
listener.onListItemOptionSelected(this, item);
|
||||
}
|
||||
|
||||
public interface OptionsMenuListener<T>{
|
||||
void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<T> item, Menu menu);
|
||||
void onListItemOptionSelected(ListItemWithOptionsMenu<T> item, MenuItem menuItem);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@@ -36,6 +37,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -113,9 +115,7 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
private void logOut(String accountID){
|
||||
AccountSessionManager.get(accountID).logOut(activity, ()->{
|
||||
dismiss();
|
||||
activity.finish();
|
||||
Intent intent=new Intent(activity, MainActivity.class);
|
||||
activity.startActivity(intent);
|
||||
((MainActivity)activity).restartHomeFragment();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,17 +248,17 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
dismiss();
|
||||
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
|
||||
dismiss();
|
||||
if(fragment!=null){
|
||||
fragment.setCurrentTab(R.id.tab_profile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null)
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null){
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
((MainActivity)activity).restartHomeFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -47,7 +47,7 @@ public class SearchViewHelper{
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 300);
|
||||
searchEdit.postDelayed(debouncer, 500);
|
||||
boolean newIsEmpty=e.length()==0;
|
||||
if(isEmpty!=newIsEmpty){
|
||||
isEmpty=newIsEmpty;
|
||||
|
||||
@@ -3,9 +3,12 @@ package org.joinmastodon.android.ui.adapters;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.AvatarPileListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.OptionsListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
@@ -13,11 +16,21 @@ import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemViewHolder<?>>{
|
||||
public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListItemViewHolder<?>> implements ImageLoaderRecyclerAdapter{
|
||||
private List<ListItem<T>> items;
|
||||
|
||||
public GenericListItemsAdapter(List<ListItem<T>> items){
|
||||
super(null);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
public GenericListItemsAdapter(ListImageLoaderWrapper imgLoader, List<ListItem<T>> items){
|
||||
super(imgLoader);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
@@ -32,6 +45,10 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
|
||||
if(viewType==R.id.list_item_radio)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
|
||||
if(viewType==R.id.list_item_options)
|
||||
return new OptionsListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_avatar_pile)
|
||||
return new AvatarPileListItemViewHolder(parent.getContext(), parent);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
@@ -51,4 +68,20 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getItemViewType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.get(image);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
@@ -24,6 +26,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
@@ -198,6 +201,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
UiUtils.openSystemShareSheet(activity, item.status.url);
|
||||
}else if(id==R.id.translate){
|
||||
item.parentFragment.togglePostTranslation(item.status, item.parentID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(activity, AddAccountToListsFragment.class, args);
|
||||
}else if(id==R.id.copy_link){
|
||||
activity.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, item.status.url));
|
||||
UiUtils.maybeShowTextCopiedToast(activity);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -326,6 +337,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
if(item.imgRequest!=null){
|
||||
crossfadeDrawable.setSize(card.width, card.height);
|
||||
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
didClear=false;
|
||||
}
|
||||
@@ -84,8 +85,14 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear && item.status.spoilerRevealed)
|
||||
if(didClear)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
Card card=item.status.card;
|
||||
// Make sure the image is not stretched if the server returned wrong dimensions
|
||||
if(drawable!=null && (drawable.getIntrinsicWidth()!=card.width || drawable.getIntrinsicHeight()!=card.height)){
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +102,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onClick(View v){
|
||||
UiUtils.launchWebBrowser(itemView.getContext(), item.status.card.url);
|
||||
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,11 +125,11 @@ public abstract class StatusDisplayItem{
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
status.spoilerRevealed=false;
|
||||
}else if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
}else if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, status, statusForContent, Type.SPOILER);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
status.spoilerRevealed=false;
|
||||
status.spoilerRevealed=!AccountSessionManager.get(accountID).getLocalPreferences().showCWs;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ public class BlurhashCrossfadeDrawable extends Drawable{
|
||||
|
||||
public void setImageDrawable(Drawable imageDrawable){
|
||||
this.imageDrawable=imageDrawable;
|
||||
if(imageDrawable!=null){
|
||||
width=imageDrawable.getIntrinsicWidth();
|
||||
height=imageDrawable.getIntrinsicHeight();
|
||||
}
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
|
||||
@@ -716,9 +716,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
public void onBind(Attachment item){
|
||||
super.onBind(item);
|
||||
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) imageView.getLayoutParams();
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
ViewImageLoader.load(this, listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()), new UrlImageLoaderRequest(item.url), false);
|
||||
Drawable currentDrawable=listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition());
|
||||
if(item.hasKnownDimensions()){
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
}else if(currentDrawable!=null){
|
||||
params.width=currentDrawable.getIntrinsicWidth();
|
||||
params.height=currentDrawable.getIntrinsicHeight();
|
||||
}else{
|
||||
params.width=1920;
|
||||
params.height=1080;
|
||||
}
|
||||
ViewImageLoader.load(this, currentDrawable, new UrlImageLoaderRequest(item.url), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -760,9 +769,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
super.onBind(item);
|
||||
playerReady=false;
|
||||
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
|
||||
Drawable currentDrawable=listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition());
|
||||
if(item.hasKnownDimensions()){
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
}else if(currentDrawable!=null){
|
||||
params.width=currentDrawable.getIntrinsicWidth();
|
||||
params.height=currentDrawable.getIntrinsicHeight();
|
||||
}else{
|
||||
params.width=1920;
|
||||
params.height=1080;
|
||||
}
|
||||
wrap.setBackground(currentDrawable);
|
||||
progressBar.setVisibility(item.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||
if(itemView.isAttachedToWindow()){
|
||||
reset();
|
||||
@@ -822,7 +840,9 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
@Override
|
||||
public boolean onError(MediaPlayer mp, int what, int extra){
|
||||
Log.e(TAG, "video player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||
return false;
|
||||
Toast.makeText(activity, R.string.error_playing_video, Toast.LENGTH_SHORT).show();
|
||||
onStartSwipeToDismissTransition(0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void prepareAndStartPlayer(){
|
||||
@@ -843,6 +863,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
player.prepareAsync();
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "Error initializing gif player", x);
|
||||
Toast.makeText(activity, R.string.error_playing_video, Toast.LENGTH_SHORT).show();
|
||||
onStartSwipeToDismissTransition(0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,8 +119,10 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
|
||||
int width=right-left;
|
||||
int height=bottom-top;
|
||||
if(width==0 || height==0)
|
||||
if(width==0 || height==0 || child.getWidth()==0 || child.getWidth()==0){
|
||||
matrix.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
float scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
|
||||
minScale=scale;
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
@@ -130,11 +131,7 @@ public class ClickableLinksDelegate implements CustomViewHelper{
|
||||
//copy link text to clipboard
|
||||
ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink()));
|
||||
//show toast, android from S_V2 on has built-in popup, as documented in
|
||||
//https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
UiUtils.maybeShowTextCopiedToast(view.getContext());
|
||||
//reset view
|
||||
resetAndInvalidate();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
|
||||
public class ActionModeHelper{
|
||||
public static ActionMode startActionMode(AppKitFragment fragment, IntSupplier statusBarColorSupplier, ActionMode.Callback callback){
|
||||
FragmentStackActivity activity=(FragmentStackActivity) fragment.getActivity();
|
||||
return activity.startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onCreateActionMode(mode, menu))
|
||||
return false;
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", statusBarColorSupplier.getAsInt(), UiUtils.getThemeColor(activity, 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();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
View fakeView=new View(activity);
|
||||
// mode.setCustomView(fakeView);
|
||||
// int buttonID=activity.getResources().getIdentifier("action_mode_close_button", "id", "android");
|
||||
// View btn=activity.getWindow().getDecorView().findViewById(buttonID);
|
||||
// if(btn!=null){
|
||||
// ((ViewGroup.MarginLayoutParams)btn.getLayoutParams()).setMarginEnd(0);
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onPrepareActionMode(mode, menu))
|
||||
return false;
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon();
|
||||
if(icon!=null){
|
||||
icon=icon.mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(activity, R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", UiUtils.getThemeColor(activity, R.attr.colorM3Primary), statusBarColorSupplier.getAsInt());
|
||||
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){
|
||||
activity.getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
callback.onDestroyActionMode(mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ public class DiscoverInfoBannerHelper{
|
||||
private final BannerType type;
|
||||
private final String accountID;
|
||||
private static EnumSet<BannerType> bannerTypesToShow=EnumSet.noneOf(BannerType.class);
|
||||
private SingleViewRecyclerAdapter bannerAdapter;
|
||||
private boolean added;
|
||||
|
||||
static{
|
||||
for(BannerType t:BannerType.values()){
|
||||
@@ -40,6 +42,8 @@ public class DiscoverInfoBannerHelper{
|
||||
}
|
||||
|
||||
public void maybeAddBanner(RecyclerView list, MergeRecyclerAdapter adapter){
|
||||
if(added)
|
||||
return;
|
||||
if(bannerTypesToShow.contains(type)){
|
||||
banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
|
||||
TextView text=banner.findViewById(R.id.banner_text);
|
||||
@@ -56,7 +60,8 @@ public class DiscoverInfoBannerHelper{
|
||||
case LOCAL_TIMELINE -> R.drawable.ic_stream_24px;
|
||||
case ACCOUNTS -> R.drawable.ic_group_add_24px;
|
||||
});
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(banner));
|
||||
adapter.addAdapter(0, bannerAdapter=new SingleViewRecyclerAdapter(banner));
|
||||
added=true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +70,13 @@ public class DiscoverInfoBannerHelper{
|
||||
// bannerTypesToShow is not updated here on purpose so the banner keeps showing until the app is relaunched
|
||||
}
|
||||
|
||||
public void removeBanner(MergeRecyclerAdapter adapter){
|
||||
if(bannerAdapter!=null){
|
||||
adapter.removeAdapter(bannerAdapter);
|
||||
added=false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
SharedPreferences.Editor e=prefs.edit();
|
||||
|
||||
@@ -27,6 +27,7 @@ public class MediaAttachmentViewController{
|
||||
private final Context context;
|
||||
private boolean didClear;
|
||||
private Status status;
|
||||
private Attachment attachment;
|
||||
|
||||
public MediaAttachmentViewController(Context context, MediaGridStatusDisplayItem.GridItemType type){
|
||||
view=context.getSystemService(LayoutInflater.class).inflate(switch(type){
|
||||
@@ -50,6 +51,7 @@ public class MediaAttachmentViewController{
|
||||
|
||||
public void bind(Attachment attachment, Status status){
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
crossfadeDrawable.setSize(attachment.getWidth(), attachment.getHeight());
|
||||
crossfadeDrawable.setBlurhashDrawable(attachment.blurhashPlaceholder);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
@@ -69,6 +71,11 @@ public class MediaAttachmentViewController{
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
// Make sure the image is not stretched if the server returned wrong dimensions
|
||||
if(drawable!=null && (drawable.getIntrinsicWidth()!=attachment.getWidth() || drawable.getIntrinsicHeight()!=attachment.getHeight())){
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearImage(){
|
||||
|
||||
@@ -45,12 +45,14 @@ import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -63,6 +65,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
@@ -615,10 +618,10 @@ public class UiUtils{
|
||||
|
||||
public static void openURL(Context context, String accountID, String url){
|
||||
Uri uri=Uri.parse(url);
|
||||
if(accountID!=null && "https".equals(uri.getScheme()) && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
|
||||
if(accountID!=null && "https".equals(uri.getScheme())){
|
||||
List<String> path=uri.getPathSegments();
|
||||
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
|
||||
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
|
||||
if(AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority()) && path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
|
||||
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
|
||||
new GetStatusByID(path.get(1))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -638,6 +641,32 @@ public class UiUtils{
|
||||
.wrapProgress((Activity)context, R.string.loading, true)
|
||||
.exec(accountID);
|
||||
return;
|
||||
}else{
|
||||
new GetSearchResults(url, null, true, null, 0, 0)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
if(result.statuses!=null && !result.statuses.isEmpty()){
|
||||
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
|
||||
Nav.go((Activity)context, ThreadFragment.class, args);
|
||||
}else if(result.accounts!=null && !result.accounts.isEmpty()){
|
||||
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
|
||||
Nav.go((Activity)context, ProfileFragment.class, args);
|
||||
}else{
|
||||
launchWebBrowser(context, url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
launchWebBrowser(context, url);
|
||||
}
|
||||
})
|
||||
.wrapProgress((Activity)context, R.string.loading, true)
|
||||
.exec(accountID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
launchWebBrowser(context, url);
|
||||
@@ -761,6 +790,17 @@ public class UiUtils{
|
||||
return insets;
|
||||
}
|
||||
|
||||
public static void applyBottomInsetToFAB(View fab, WindowInsets insets){
|
||||
int inset;
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 /*&& wantsOverlaySystemNavigation()*/){
|
||||
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||
inset=bottomInset>0 ? Math.max(V.dp(40), bottomInset) : 0;
|
||||
}else{
|
||||
inset=0;
|
||||
}
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||
}
|
||||
|
||||
public static String formatDuration(Context context, int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
@@ -783,4 +823,12 @@ public class UiUtils{
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_toot_title)));
|
||||
}
|
||||
|
||||
public static void maybeShowTextCopiedToast(Context context){
|
||||
//show toast, android from S_V2 on has built-in popup, as documented in
|
||||
//https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
|
||||
if(Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2){
|
||||
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public class ComposeMediaViewController{
|
||||
updateMediaAttachmentsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean addMediaAttachment(Uri uri, String description){
|
||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS){
|
||||
showMediaAttachmentError(fragment.getResources().getQuantityString(R.plurals.cant_add_more_than_x_attachments, MAX_ATTACHMENTS, MAX_ATTACHMENTS));
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class DropdownSubmenuController{
|
||||
protected List<Item<?>> items;
|
||||
protected LinearLayout contentView;
|
||||
protected UsableRecyclerView list;
|
||||
protected TextView backItem;
|
||||
protected final ToolbarDropdownMenuController dropdownController;
|
||||
protected MergeRecyclerAdapter mergeAdapter;
|
||||
protected ItemsAdapter itemsAdapter;
|
||||
|
||||
public DropdownSubmenuController(ToolbarDropdownMenuController dropdownController){
|
||||
this.dropdownController=dropdownController;
|
||||
}
|
||||
|
||||
protected abstract CharSequence getBackItemTitle();
|
||||
public void onDismiss(){}
|
||||
|
||||
protected void createView(){
|
||||
contentView=new LinearLayout(dropdownController.getActivity());
|
||||
contentView.setOrientation(LinearLayout.VERTICAL);
|
||||
CharSequence backTitle=getBackItemTitle();
|
||||
if(!TextUtils.isEmpty(backTitle)){
|
||||
backItem=(TextView) dropdownController.getActivity().getLayoutInflater().inflate(R.layout.item_dropdown_menu, contentView, false);
|
||||
((LinearLayout.LayoutParams) backItem.getLayoutParams()).topMargin=V.dp(8);
|
||||
backItem.setText(backTitle);
|
||||
backItem.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_arrow_back, 0, 0, 0);
|
||||
backItem.setBackground(UiUtils.getThemeDrawable(dropdownController.getActivity(), android.R.attr.selectableItemBackground));
|
||||
backItem.setOnClickListener(v->dropdownController.popSubmenuController());
|
||||
backItem.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setText(info.getText()+". "+host.getResources().getString(R.string.back));
|
||||
}
|
||||
});
|
||||
contentView.addView(backItem);
|
||||
}
|
||||
list=new UsableRecyclerView(dropdownController.getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(dropdownController.getActivity()));
|
||||
itemsAdapter=new ItemsAdapter();
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(itemsAdapter);
|
||||
list.setAdapter(mergeAdapter);
|
||||
list.setPadding(0, backItem!=null ? 0 : V.dp(8), 0, V.dp(8));
|
||||
list.setClipToPadding(false);
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private final Paint paint=new Paint();
|
||||
{
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(UiUtils.getThemeColor(dropdownController.getActivity(), R.attr.colorM3OutlineVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View view=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
paint.setAlpha(Math.round(view.getAlpha()*255));
|
||||
float y=view.getTop()-V.dp(8)-paint.getStrokeWidth()/2f;
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
outRect.top=V.dp(17);
|
||||
}
|
||||
}
|
||||
});
|
||||
contentView.addView(list, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
if(contentView==null)
|
||||
createView();
|
||||
return contentView;
|
||||
}
|
||||
|
||||
protected HideableSingleViewRecyclerAdapter createEmptyView(@DrawableRes int icon, @StringRes int title, @StringRes int subtitle){
|
||||
View view=dropdownController.getActivity().getLayoutInflater().inflate(R.layout.popup_menu_empty, list, false);
|
||||
ImageView iconView=view.findViewById(R.id.icon);
|
||||
TextView titleView=view.findViewById(R.id.title);
|
||||
TextView subtitleView=view.findViewById(R.id.subtitle);
|
||||
iconView.setImageResource(icon);
|
||||
titleView.setText(title);
|
||||
subtitleView.setText(subtitle);
|
||||
return new HideableSingleViewRecyclerAdapter(view);
|
||||
}
|
||||
|
||||
protected final class Item<T>{
|
||||
public final String title;
|
||||
public final boolean hasSubmenu;
|
||||
public final boolean dividerBefore;
|
||||
public final T parentObject;
|
||||
public final Consumer<Item<T>> onClick;
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, T parentObject, Consumer<Item<T>> onClick){
|
||||
this.title=title;
|
||||
this.hasSubmenu=hasSubmenu;
|
||||
this.dividerBefore=dividerBefore;
|
||||
this.parentObject=parentObject;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(title, hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
public Item(@StringRes int titleRes, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(dropdownController.getActivity().getString(titleRes), hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
private void performClick(){
|
||||
onClick.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected class ItemsAdapter extends RecyclerView.Adapter<ItemHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemHolder extends BindableViewHolder<Item<?>> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
|
||||
public ItemHolder(){
|
||||
super(dropdownController.getActivity(), R.layout.item_dropdown_menu, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item<?> item){
|
||||
text.setText(item.title);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, item.hasSubmenu ? R.drawable.ic_arrow_right_24px : 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||
import org.joinmastodon.android.fragments.ManageFollowedHashtagsFragment;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
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.utils.V;
|
||||
|
||||
public class HomeTimelineHashtagsMenuController extends DropdownSubmenuController{
|
||||
private HideableSingleViewRecyclerAdapter largeProgressAdapter;
|
||||
private HideableSingleViewRecyclerAdapter emptyAdapter;
|
||||
private APIRequest<?> currentRequest;
|
||||
|
||||
public HomeTimelineHashtagsMenuController(ToolbarDropdownMenuController dropdownController){
|
||||
super(dropdownController);
|
||||
items=new ArrayList<>();
|
||||
loadHashtags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createView(){
|
||||
super.createView();
|
||||
emptyAdapter=createEmptyView(R.drawable.ic_tag_24px, R.string.no_followed_hashtags_title, R.string.no_followed_hashtags_subtitle);
|
||||
FrameLayout largeProgressView=new FrameLayout(dropdownController.getActivity());
|
||||
int pad=V.dp(32);
|
||||
largeProgressView.setPadding(0, pad, 0, pad);
|
||||
largeProgressView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
ProgressBar progress=new ProgressBar(dropdownController.getActivity());
|
||||
largeProgressView.addView(progress, new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER));
|
||||
largeProgressAdapter=new HideableSingleViewRecyclerAdapter(largeProgressView);
|
||||
mergeAdapter.addAdapter(0, largeProgressAdapter);
|
||||
emptyAdapter.setVisible(false);
|
||||
mergeAdapter.addAdapter(0, emptyAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.followed_hashtags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onTagClick(Item<Hashtag> item){
|
||||
dropdownController.dismiss();
|
||||
UiUtils.openHashtagTimeline(dropdownController.getActivity(), dropdownController.getAccountID(), item.parentObject);
|
||||
}
|
||||
|
||||
private void onManageTagsClick(){
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageFollowedHashtagsFragment.class, args);
|
||||
}
|
||||
|
||||
private void loadHashtags(){
|
||||
currentRequest=new GetFollowedTags(null, 200)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
currentRequest=null;
|
||||
dropdownController.resizeOnNextFrame();
|
||||
largeProgressAdapter.setVisible(false);
|
||||
((List<Hashtag>) result).sort(Comparator.comparing(tag->tag.name));
|
||||
int prevSize=items.size();
|
||||
for(Hashtag tag:result){
|
||||
items.add(new Item<>("#"+tag.name, false, false, tag, HomeTimelineHashtagsMenuController.this::onTagClick));
|
||||
}
|
||||
items.add(new Item<Void>(R.string.manage_hashtags, false, true, i->onManageTagsClick()));
|
||||
itemsAdapter.notifyItemRangeInserted(prevSize, result.size()+1);
|
||||
emptyAdapter.setVisible(result.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity activity=dropdownController.getActivity();
|
||||
if(activity!=null)
|
||||
error.showToast(activity);
|
||||
dropdownController.popSubmenuController();
|
||||
|
||||
}
|
||||
})
|
||||
.exec(dropdownController.getAccountID());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.CreateListFragment;
|
||||
import org.joinmastodon.android.fragments.ManageListsFragment;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class HomeTimelineListsMenuController extends DropdownSubmenuController{
|
||||
private final List<FollowList> lists;
|
||||
private final HomeTimelineMenuController.Callback callback;
|
||||
private HideableSingleViewRecyclerAdapter emptyAdapter;
|
||||
|
||||
public HomeTimelineListsMenuController(ToolbarDropdownMenuController dropdownController, HomeTimelineMenuController.Callback callback){
|
||||
super(dropdownController);
|
||||
this.lists=new ArrayList<>(callback.getLists());
|
||||
this.callback=callback;
|
||||
items=new ArrayList<>();
|
||||
for(FollowList l:lists){
|
||||
items.add(new Item<>(l.title, false, false, l, this::onListSelected));
|
||||
}
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.create_list), false, true, i->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), CreateListFragment.class, args);
|
||||
}));
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.manage_lists), false, false, i->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageListsFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.lists);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createView(){
|
||||
super.createView();
|
||||
emptyAdapter=createEmptyView(R.drawable.ic_list_alt_24px, R.string.no_lists_title, R.string.no_lists_subtitle);
|
||||
if(lists.isEmpty()){
|
||||
mergeAdapter.addAdapter(0, emptyAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
private void onListSelected(Item<FollowList> item){
|
||||
callback.onListSelected(item.parentObject);
|
||||
dropdownController.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HomeTimelineMenuController extends DropdownSubmenuController{
|
||||
private Callback callback;
|
||||
|
||||
public HomeTimelineMenuController(ToolbarDropdownMenuController dropdownController, Callback callback){
|
||||
super(dropdownController);
|
||||
this.callback=callback;
|
||||
items=List.of(
|
||||
new Item<Void>(R.string.timeline_following, false, false, i->{
|
||||
callback.onFollowingSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.local_timeline, false, false, i->{
|
||||
callback.onLocalSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.lists, true, true, i->dropdownController.pushSubmenuController(new HomeTimelineListsMenuController(dropdownController, callback))),
|
||||
new Item<Void>(R.string.followed_hashtags, true, false, i->dropdownController.pushSubmenuController(new HomeTimelineHashtagsMenuController(dropdownController)))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return null;
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onFollowingSelected();
|
||||
void onLocalSelected();
|
||||
List<FollowList> getLists();
|
||||
void onListSelected(FollowList list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ToolbarDropdownMenuController{
|
||||
private final HostFragment fragment;
|
||||
private FrameLayout windowView;
|
||||
private FrameLayout menuContainer;
|
||||
private boolean dismissing;
|
||||
private List<DropdownSubmenuController> controllerStack=new ArrayList<>();
|
||||
private Animator currentTransition;
|
||||
|
||||
public ToolbarDropdownMenuController(HostFragment fragment){
|
||||
this.fragment=fragment;
|
||||
}
|
||||
|
||||
public void show(DropdownSubmenuController initialSubmenu){
|
||||
if(windowView!=null)
|
||||
return;
|
||||
|
||||
menuContainer=new FrameLayout(fragment.getActivity());
|
||||
menuContainer.setBackgroundResource(R.drawable.bg_m3_surface2);
|
||||
menuContainer.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
menuContainer.setClipToOutline(true);
|
||||
menuContainer.setElevation(V.dp(6));
|
||||
View menuView=initialSubmenu.getView();
|
||||
menuView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(menuView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
windowView=new WindowView(fragment.getActivity());
|
||||
int pad=V.dp(16);
|
||||
windowView.setPadding(pad, fragment.getToolbar().getHeight(), pad, pad);
|
||||
windowView.setClipToPadding(false);
|
||||
windowView.addView(menuContainer, new FrameLayout.LayoutParams(V.dp(200), ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP | Gravity.START));
|
||||
|
||||
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
|
||||
wlp.format=PixelFormat.TRANSLUCENT;
|
||||
wlp.token=fragment.getActivity().getWindow().getDecorView().getWindowToken();
|
||||
wlp.width=wlp.height=ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR;
|
||||
wlp.setTitle(fragment.getActivity().getString(R.string.dropdown_menu));
|
||||
fragment.getActivity().getWindowManager().addView(windowView, wlp);
|
||||
|
||||
menuContainer.setPivotX(V.dp(100));
|
||||
menuContainer.setPivotY(0);
|
||||
menuContainer.setScaleX(.8f);
|
||||
menuContainer.setScaleY(.8f);
|
||||
menuContainer.setAlpha(0f);
|
||||
menuContainer.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.alpha(1f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.start();
|
||||
controllerStack.add(initialSubmenu);
|
||||
}
|
||||
|
||||
public void dismiss(){
|
||||
if(windowView==null || dismissing)
|
||||
return;
|
||||
dismissing=true;
|
||||
fragment.onDropdownWillDismiss();
|
||||
menuContainer.animate()
|
||||
.scaleX(.8f)
|
||||
.scaleY(.8f)
|
||||
.alpha(0f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.withEndAction(()->{
|
||||
controllerStack.clear();
|
||||
fragment.getActivity().getWindowManager().removeView(windowView);
|
||||
menuContainer.removeAllViews();
|
||||
dismissing=false;
|
||||
windowView=null;
|
||||
menuContainer=null;
|
||||
fragment.onDropdownDismissed();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
public void pushSubmenuController(DropdownSubmenuController controller){
|
||||
View prevView=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View newView=controller.getView();
|
||||
newView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(newView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
controllerStack.add(controller);
|
||||
animateTransition(prevView, newView, true);
|
||||
}
|
||||
|
||||
public void popSubmenuController(){
|
||||
if(menuContainer.getChildCount()<=1)
|
||||
throw new IllegalStateException();
|
||||
DropdownSubmenuController controller=controllerStack.remove(controllerStack.size()-1);
|
||||
controller.onDismiss();
|
||||
View top=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View prev=menuContainer.getChildAt(menuContainer.getChildCount()-2);
|
||||
prev.setVisibility(View.VISIBLE);
|
||||
animateTransition(prev, top, false);
|
||||
}
|
||||
|
||||
private void animateTransition(View bottomView, View topView, boolean adding){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
private final Rect tmpRect=new Rect();
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
ObjectAnimator slideIn;
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getTop()+(adding ? topView : bottomView).getHeight()),
|
||||
slideIn=ObjectAnimator.ofFloat(topView, View.TRANSLATION_X, adding ? menuContainer.getWidth() : 0, adding ? 0 : menuContainer.getWidth()),
|
||||
ObjectAnimator.ofFloat(bottomView, View.TRANSLATION_X, adding ? 0 : -menuContainer.getWidth()/4f, adding ? -menuContainer.getWidth()/4f : 0),
|
||||
ObjectAnimator.ofFloat(bottomView, View.ALPHA, adding ? 1f : 0f, adding ? 0f : 1f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
bottomView.setClipBounds(null);
|
||||
bottomView.setTranslationX(0);
|
||||
bottomView.setAlpha(1f);
|
||||
topView.setTranslationX(0);
|
||||
topView.setAlpha(1f);
|
||||
if(adding){
|
||||
bottomView.setVisibility(View.GONE);
|
||||
}else{
|
||||
menuContainer.removeView(topView);
|
||||
}
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
slideIn.addUpdateListener(animation->{
|
||||
tmpRect.set(0, 0, Math.round(topView.getX()-bottomView.getX()), bottomView.getHeight());
|
||||
bottomView.setClipBounds(tmpRect);
|
||||
});
|
||||
currentTransition=set;
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void resizeOnNextFrame(){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getBottom());
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
currentTransition=anim;
|
||||
anim.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Activity getActivity(){
|
||||
return fragment.getActivity();
|
||||
}
|
||||
|
||||
String getAccountID(){
|
||||
return fragment.getAccountID();
|
||||
}
|
||||
|
||||
private class WindowView extends FrameLayout{
|
||||
private final Rect tmpRect=new Rect();
|
||||
public WindowView(@NonNull Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
child.getHitRect(tmpRect);
|
||||
if(tmpRect.contains(Math.round(ev.getX()), Math.round(ev.getY())))
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
if(ev.getAction()==MotionEvent.ACTION_DOWN){
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev){
|
||||
if(currentTransition!=null)
|
||||
return false;
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
if(controllerStack.size()>1)
|
||||
popSubmenuController();
|
||||
else
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
public interface HostFragment{
|
||||
// Fragment methods
|
||||
Activity getActivity();
|
||||
Resources getResources();
|
||||
Toolbar getToolbar();
|
||||
String getAccountID();
|
||||
|
||||
// Callbacks
|
||||
void onDropdownWillDismiss();
|
||||
void onDropdownDismissed();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
@@ -26,6 +27,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -40,6 +42,7 @@ import org.parceler.Parcels;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -58,12 +61,15 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View checkbox;
|
||||
private final ProgressBar actionProgress;
|
||||
private final ImageButton menuButton;
|
||||
|
||||
private final String accountID;
|
||||
private final Fragment fragment;
|
||||
private final HashMap<String, Relationship> relationships;
|
||||
|
||||
private Consumer<AccountViewHolder> onClick;
|
||||
private Predicate<AccountViewHolder> onLongClick;
|
||||
private Consumer<MenuItem> onCustomMenuItemSelected;
|
||||
private AccessoryType accessoryType;
|
||||
private boolean showBio;
|
||||
private boolean checked;
|
||||
@@ -85,6 +91,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio=findViewById(R.id.bio);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
menuButton=findViewById(R.id.options_btn);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
@@ -94,6 +101,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
menuButton.setOnClickListener(v->showMenuFromButton());
|
||||
|
||||
setStyle(AccessoryType.BUTTON, false);
|
||||
}
|
||||
@@ -181,37 +189,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
if(relationships==null)
|
||||
if(onLongClick!=null && onLongClick.test(this))
|
||||
return true;
|
||||
if(accessoryType==AccessoryType.MENU || !prepareMenu())
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -226,7 +210,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
public void setActionProgressVisible(boolean visible){
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(button.getTextColors());
|
||||
button.setTextVisible(!visible);
|
||||
@@ -279,6 +263,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
})
|
||||
.wrapProgress(fragment.getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(fragment.getActivity(), AddAccountToListsFragment.class, args);
|
||||
}else if(onCustomMenuItemSelected!=null){
|
||||
onCustomMenuItemSelected.accept(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -292,6 +283,14 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
onClick=listener;
|
||||
}
|
||||
|
||||
public void setOnLongClickListener(Predicate<AccountViewHolder> onLongClick){
|
||||
this.onLongClick=onLongClick;
|
||||
}
|
||||
|
||||
public void setOnCustomMenuItemSelectedListener(Consumer<MenuItem> onCustomMenuItemSelected){
|
||||
this.onCustomMenuItemSelected=onCustomMenuItemSelected;
|
||||
}
|
||||
|
||||
public void setStyle(AccessoryType accessoryType, boolean showBio){
|
||||
if(accessoryType!=this.accessoryType){
|
||||
this.accessoryType=accessoryType;
|
||||
@@ -299,20 +298,29 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
case NONE -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case CHECKBOX -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new CheckBox(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case RADIOBUTTON -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new RadioButton(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case BUTTON -> {
|
||||
case BUTTON, CUSTOM_BUTTON -> {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case MENU -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
view.setCheckable(accessoryType==AccessoryType.CHECKBOX || accessoryType==AccessoryType.RADIOBUTTON);
|
||||
@@ -321,15 +329,68 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio.setVisibility(showBio ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private boolean prepareMenu(){
|
||||
if(relationships==null)
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showMenuFromButton(){
|
||||
if(!prepareMenu())
|
||||
return;
|
||||
int[] xy={0, 0};
|
||||
itemView.getLocationInWindow(xy);
|
||||
int x=xy[0], y=xy[1];
|
||||
menuButton.getLocationInWindow(xy);
|
||||
menuAnchor.setTranslationX(xy[0]-x+menuButton.getWidth()/2f);
|
||||
menuAnchor.setTranslationY(xy[1]-y+menuButton.getHeight());
|
||||
contextMenu.show();
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
view.setChecked(checked);
|
||||
}
|
||||
|
||||
public PopupMenu getContextMenu(){
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public ProgressBarButton getButton(){
|
||||
return button;
|
||||
}
|
||||
|
||||
public enum AccessoryType{
|
||||
NONE,
|
||||
BUTTON,
|
||||
CHECKBOX,
|
||||
RADIOBUTTON
|
||||
RADIOBUTTON,
|
||||
MENU,
|
||||
CUSTOM_BUTTON
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.ui.views.AvatarPileView;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AvatarPileListItemViewHolder extends ListItemViewHolder<AvatarPileListItem<?>> implements ImageLoaderViewHolder{
|
||||
private final AvatarPileView pile;
|
||||
|
||||
public AvatarPileListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list, parent);
|
||||
pile=new AvatarPileView(context);
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
lp.topMargin=lp.bottomMargin=V.dp(-8);
|
||||
view.addView(pile, lp);
|
||||
view.setClipToPadding(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AvatarPileListItem<?> item){
|
||||
super.onBind(item);
|
||||
pile.setVisibleAvatarCount(item.avatars.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
pile.avatars[index].setImageDrawable(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
pile.avatars[index].setImageResource(R.drawable.image_placeholder);
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,6 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
|
||||
public class OptionsListItemViewHolder extends ListItemViewHolder<ListItemWithOptionsMenu<?>>{
|
||||
private final PopupMenu menu;
|
||||
private final ImageButton menuBtn;
|
||||
|
||||
public OptionsListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list_options, parent);
|
||||
menuBtn=findViewById(R.id.options_btn);
|
||||
menu=new PopupMenu(context, menuBtn);
|
||||
menuBtn.setOnClickListener(this::onMenuBtnClick);
|
||||
|
||||
menu.setOnMenuItemClickListener(menuItem->{
|
||||
item.performItemSelected(menuItem);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void onMenuBtnClick(View v){
|
||||
menu.getMenu().clear();
|
||||
item.performConfigureMenu(menu.getMenu());
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user