fix akkoma crash on list edit

closes sk22#352
This commit is contained in:
sk
2023-01-23 06:51:24 -03:00
committed by LucasGGamerM
parent b9a997730a
commit 5c8c888024
5 changed files with 221 additions and 228 deletions

View File

@@ -423,7 +423,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
} else if ((list = listItems.get(id)) != null) { } else if ((list = listItems.get(id)) != null) {
args.putString("listID", list.id); args.putString("listID", list.id);
args.putString("listTitle", list.title); args.putString("listTitle", list.title);
args.putInt("repliesPolicy", list.repliesPolicy.ordinal()); if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args); Nav.go(getActivity(), ListTimelineFragment.class, args);
} else if ((hashtag = hashtagsItems.get(id)) != null) { } else if ((hashtag = hashtagsItems.get(id)) != null) {
args.putString("hashtag", hashtag.name); args.putString("hashtag", hashtag.name);

View File

@@ -2,16 +2,15 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences; import androidx.annotation.Nullable;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetList; import org.joinmastodon.android.api.requests.lists.GetList;
import org.joinmastodon.android.api.requests.lists.UpdateList; import org.joinmastodon.android.api.requests.lists.UpdateList;
@@ -23,7 +22,6 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ListTimelineEditor; import org.joinmastodon.android.ui.views.ListTimelineEditor;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@@ -36,9 +34,9 @@ import me.grishka.appkit.utils.V;
public class ListTimelineFragment extends PinnableStatusListFragment { public class ListTimelineFragment extends PinnableStatusListFragment {
private String listID; private String listID;
private String listTitle; private String listTitle;
@Nullable
private ListTimeline.RepliesPolicy repliesPolicy; private ListTimeline.RepliesPolicy repliesPolicy;
private ImageButton fab; private ImageButton fab;
private Bundle resultArgs = new Bundle();
public ListTimelineFragment() { public ListTimelineFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab); setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -51,7 +49,6 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
listID = args.getString("listID"); listID = args.getString("listID");
listTitle = args.getString("listTitle"); listTitle = args.getString("listTitle");
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)]; repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
resultArgs.putString("listID", listID);
setTitle(listTitle); setTitle(listTitle);
setHasOptionsMenu(true); setHasOptionsMenu(true);
@@ -61,7 +58,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
public void onSuccess(ListTimeline listTimeline) { public void onSuccess(ListTimeline listTimeline) {
// TODO: save updated info // TODO: save updated info
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title); if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
if (!listTimeline.repliesPolicy.equals(repliesPolicy)) repliesPolicy = listTimeline.repliesPolicy; if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
repliesPolicy = listTimeline.repliesPolicy;
}
} }
@Override @Override
@@ -88,41 +87,39 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
.setTitle(R.string.sk_edit_list_title) .setTitle(R.string.sk_edit_list_title)
.setIcon(R.drawable.ic_fluent_people_list_28_regular) .setIcon(R.drawable.ic_fluent_people_list_28_regular)
.setView(editor) .setView(editor)
.setPositiveButton(R.string.save, (d, which) -> { .setPositiveButton(R.string.save, (d, which) ->
new UpdateList(listID, editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() { new UpdateList(listID, editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override @Override
public void onSuccess(ListTimeline list) { public void onSuccess(ListTimeline list) {
setTitle(list.title); setTitle(list.title);
listTitle = list.title; listTitle = list.title;
repliesPolicy = list.repliesPolicy; repliesPolicy = list.repliesPolicy;
resultArgs.putString("listTitle", listTitle); Bundle result = new Bundle();
resultArgs.putInt("repliesPolicy", repliesPolicy.ordinal()); result.putString("listID", listID);
setResult(true, resultArgs); result.putString("listTitle", listTitle);
} if (repliesPolicy != null) result.putInt("repliesPolicy", repliesPolicy.ordinal());
setResult(true, result);
}
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {
error.showToast(getContext()); error.showToast(getContext());
} }
}).exec(accountID); }).exec(accountID))
})
.setNegativeButton(R.string.cancel, (d, which) -> {}) .setNegativeButton(R.string.cancel, (d, which) -> {})
.show(); .show();
} else if (item.getItemId() == R.id.delete) { } else if (item.getItemId() == R.id.delete) {
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> { UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
resultArgs.putBoolean("deleted", true); Bundle result = new Bundle();
setResult(true, resultArgs); result.putBoolean("deleted", true);
result.putString("listID", listID);
setResult(true, result);
Nav.finish(this); Nav.finish(this);
}); });
} }
return true; return true;
} }
@Override
public Bundle getResultArgs() {
return resultArgs;
}
@Override @Override
protected TimelineDefinition makeTimelineDefinition() { protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofList(listID, listTitle); return TimelineDefinition.ofList(listID, listTitle);

View File

@@ -21,7 +21,6 @@ import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ListTimelineEditor; import org.joinmastodon.android.ui.views.ListTimelineEditor;
import java.util.ArrayList; import java.util.ArrayList;
@@ -38,218 +37,210 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop { public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
private static final int LIST_CHANGED_RESULT = 987; private static final int LIST_CHANGED_RESULT = 987;
private String accountId; private String accountId;
private String profileAccountId; private String profileAccountId;
private String profileDisplayUsername; private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private HashMap<String, Boolean> userInListBefore = new HashMap<>(); private final HashMap<String, Boolean> userInList = new HashMap<>();
private HashMap<String, Boolean> userInList = new HashMap<>(); private ListsAdapter adapter;
private int inProgress = 0;
private ListsAdapter adapter;
private boolean pinnedUpdated;
public ListTimelinesFragment() { public ListTimelinesFragment() {
super(10); super(10);
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bundle args=getArguments(); Bundle args=getArguments();
accountId=args.getString("account"); accountId=args.getString("account");
setHasOptionsMenu(true); setHasOptionsMenu(true);
if(args.containsKey("profileAccount")){ if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount"); profileAccountId=args.getString("profileAccount");
profileDisplayUsername=args.getString("profileDisplayUsername"); String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername)); setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else { } else {
setTitle(R.string.sk_your_lists); setTitle(R.string.sk_your_lists);
} }
} }
@Override @Override
protected void onShown(){ protected void onShown(){
super.onShown(); super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData(); loadData();
} }
@Override @Override
public void onDestroy() { public void onViewCreated(View view, Bundle savedInstanceState) {
super.onDestroy(); super.onViewCreated(view, savedInstanceState);
if (pinnedUpdated) UiUtils.restartApp(); list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onViewCreated(view, savedInstanceState); inflater.inflate(R.menu.menu_list, menu);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16)); }
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public boolean onOptionsItemSelected(MenuItem item) {
inflater.inflate(R.menu.menu_list, menu); if (item.getItemId() == R.id.create) {
} ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
saveListMembership(list.id, true);
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public void onError(ErrorResponse error) {
if (item.getItemId() == R.id.create) { error.showToast(getContext());
ListTimelineEditor editor = new ListTimelineEditor(getContext()); }
new M3AlertDialogBuilder(getActivity()) }).exec(accountId)
.setTitle(R.string.sk_create_list_title) )
.setIcon(R.drawable.ic_fluent_people_add_28_regular) .setNegativeButton(R.string.cancel, (d, which) -> {})
.setView(editor) .show();
.setPositiveButton(R.string.sk_create, (d, which) -> { }
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() { return true;
@Override }
public void onSuccess(ListTimeline list) {
saveListMembership(list.id, true);
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
}
@Override private void saveListMembership(String listId, boolean isMember) {
public void onError(ErrorResponse error) { userInList.put(listId, isMember);
error.showToast(getContext()); List<String> accountIdList = Collections.singletonList(profileAccountId);
} MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
}).exec(accountId); req.setCallback(new SimpleCallback<>(this) {
}) @Override
.setNegativeButton(R.string.cancel, (d, which) -> {}) public void onSuccess(Object o) {}
.show(); }).exec(accountId);
} }
return true;
}
private void saveListMembership(String listId, boolean isMember) { @Override
userInList.put(listId, isMember); protected void doLoadData(int offset, int count){
List<String> accountIdList = Collections.singletonList(profileAccountId); userInListBefore.clear();
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList); userInList.clear();
req.setCallback(new SimpleCallback<>(this) { currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
@Override .setCallback(new SimpleCallback<>(this) {
public void onSuccess(Object o) {} @Override
}).exec(accountId); public void onSuccess(List<ListTimeline> lists) {
} for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
@Override currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
protected void doLoadData(int offset, int count){ @Override
userInListBefore.clear(); public void onSuccess(List<ListTimeline> allLists) {
userInList.clear(); List<ListTimeline> newLists = new ArrayList<>();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists()) for (ListTimeline l : allLists) {
.setCallback(new SimpleCallback<>(this) { if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
@Override if (!userInListBefore.containsKey(l.id)) {
public void onSuccess(List<ListTimeline> lists) { userInListBefore.put(l.id, false);
for (ListTimeline l : lists) userInListBefore.put(l.id, true); }
userInList.putAll(userInListBefore); }
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false); userInList.putAll(userInListBefore);
if (profileAccountId == null) return; onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) { @Override
@Override public void onFragmentResult(int reqCode, boolean listChanged, Bundle result){
public void onSuccess(List<ListTimeline> allLists) { if (reqCode == LIST_CHANGED_RESULT && listChanged) {
List<ListTimeline> newLists = new ArrayList<>(); String listID = result.getString("listID");
for (ListTimeline l : allLists) { for (int i = 0; i < data.size(); i++) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l); ListTimeline item = data.get(i);
if (!userInListBefore.containsKey(l.id)) { if (item.id.equals(listID)) {
userInListBefore.put(l.id, false); if (result.getBoolean("deleted")) {
} data.remove(i);
} adapter.notifyItemRemoved(i);
userInList.putAll(userInListBefore); } else {
onDataLoaded(newLists, false); item.title = result.getString("listTitle", item.title);
} if (result.containsKey("repliesPolicy")) {
}).exec(accountId); item.repliesPolicy = ListTimeline.RepliesPolicy.values()[result.getInt("repliesPolicy")];
} }
}) adapter.notifyItemChanged(i);
.exec(accountId); }
} break;
}
}
}
}
@Override @Override
public void onFragmentResult(int reqCode, boolean listChanged, Bundle result){ protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
if (reqCode == LIST_CHANGED_RESULT && listChanged) { return adapter = new ListsAdapter();
if (result.getBoolean("pinnedUpdated")) pinnedUpdated = true; }
String listID = result.getString("listID");
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(listID)) {
if (result.getBoolean("deleted")) {
data.remove(i);
adapter.notifyItemRemoved(i);
} else {
item.title = result.getString("listTitle", item.title);
item.repliesPolicy = ListTimeline.RepliesPolicy.values()[result.getInt("repliesPolicy")];
adapter.notifyItemChanged(i);
}
break;
}
}
}
}
@Override @Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() { public void scrollToTop() {
return adapter = new ListsAdapter(); smoothScrollRecyclerViewToTop(list);
} }
@Override private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
public void scrollToTop() { @NonNull
smoothScrollRecyclerViewToTop(list); @Override
} public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{ @Override
@NonNull public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
@Override holder.bind(data.get(position));
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ }
return new ListViewHolder();
}
@Override @Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) { public int getItemCount() {
holder.bind(data.get(position)); return data.size();
} }
}
@Override private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
public int getItemCount() { private final TextView title;
return data.size(); private final CheckBox listToggle;
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{ public ListViewHolder(){
private final TextView title; super(getActivity(), R.layout.item_text, list);
private final CheckBox listToggle; title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
public ListViewHolder(){ @Override
super(getActivity(), R.layout.item_text, list); public void onBind(ListTimeline item) {
title=findViewById(R.id.title); title.setText(item.title);
listToggle=findViewById(R.id.list_toggle); title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_list_24_regular), null, null, null);
} if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
@Override private void onClickToggle(View view) {
public void onBind(ListTimeline item) { saveListMembership(item.id, listToggle.isChecked());
title.setText(item.title); }
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_list_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) { @Override
saveListMembership(item.id, listToggle.isChecked()); public void onClick() {
} Bundle args=new Bundle();
args.putString("account", accountId);
@Override args.putString("listID", item.id);
public void onClick() { args.putString("listTitle", item.title);
Bundle args=new Bundle(); if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
args.putString("account", accountId); Nav.goForResult(getActivity(), ListTimelineFragment.class, args, LIST_CHANGED_RESULT, ListTimelinesFragment.this);
args.putString("listID", item.id); }
args.putString("listTitle", item.title); }
args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.goForResult(getActivity(), ListTimelineFragment.class, args, LIST_CHANGED_RESULT, ListTimelinesFragment.this);
}
}
} }

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
@@ -11,9 +13,9 @@ public class ListTimeline extends BaseModel {
public String id; public String id;
@RequiredField @RequiredField
public String title; public String title;
@RequiredField
public RepliesPolicy repliesPolicy; public RepliesPolicy repliesPolicy;
@NonNull
@Override @Override
public String toString() { public String toString() {
return "List{" + return "List{" +

View File

@@ -10,6 +10,9 @@ import android.widget.Button;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.ListTimeline;
@@ -37,9 +40,9 @@ public class ListTimelineEditor extends LinearLayout {
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST); setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
} }
public void applyList(String title, ListTimeline.RepliesPolicy policy) { public void applyList(String title, @Nullable ListTimeline.RepliesPolicy policy) {
input.getEditText().setText(title); input.getEditText().setText(title);
setRepliesPolicy(policy); if (policy != null) setRepliesPolicy(policy);
} }
public String getTitle() { public String getTitle() {
@@ -50,7 +53,7 @@ public class ListTimelineEditor extends LinearLayout {
return policy; return policy;
} }
public void setRepliesPolicy(ListTimeline.RepliesPolicy policy) { public void setRepliesPolicy(@NonNull ListTimeline.RepliesPolicy policy) {
this.policy = policy; this.policy = policy;
switch (policy) { switch (policy) {
case FOLLOWED -> button.setText(R.string.sk_list_replies_policy_followed); case FOLLOWED -> button.setText(R.string.sk_list_replies_policy_followed);