support exclusive lists

closes sk22#576
This commit is contained in:
sk
2023-06-15 19:21:26 +02:00
parent b463ef65ce
commit 6595a088fb
12 changed files with 112 additions and 24 deletions

View File

@@ -4,16 +4,18 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class CreateList extends MastodonAPIRequest<ListTimeline> {
public CreateList(String title, ListTimeline.RepliesPolicy repliesPolicy) {
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.POST, "/lists", ListTimeline.class);
Request req = new Request();
req.title = title;
req.exclusive = exclusive;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
}
public static class Request {
public String title;
public boolean exclusive;
public ListTimeline.RepliesPolicy repliesPolicy;
}
}

View File

@@ -4,10 +4,11 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
public UpdateList(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
CreateList.Request req = new CreateList.Request();
req.title = title;
req.exclusive = exclusive;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
}

View File

@@ -6,10 +6,12 @@ public class ListUpdatedCreatedEvent {
public final String id;
public final String title;
public final ListTimeline.RepliesPolicy repliesPolicy;
public final boolean exclusive;
public ListUpdatedCreatedEvent(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
this.id = id;
this.title = title;
this.exclusive = exclusive;
this.repliesPolicy = repliesPolicy;
}
}

View File

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

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
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.ListEditor;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
@@ -36,12 +36,12 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ListTimelineFragment extends PinnableStatusListFragment {
private String listID;
private String listTitle;
@Nullable
private ListTimeline.RepliesPolicy repliesPolicy;
private boolean exclusive;
@Override
protected boolean wantsComposeButton() {
@@ -54,6 +54,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
Bundle args = getArguments();
listID = args.getString("listID");
listTitle = args.getString("listTitle");
exclusive = args.getBoolean("listIsExclusive");
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
setTitle(listTitle);
@@ -88,8 +89,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
public boolean onOptionsItemSelected(MenuItem item) {
if (super.onOptionsItemSelected(item)) return true;
if (item.getItemId() == R.id.edit) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
editor.applyList(listTitle, repliesPolicy);
ListEditor editor = new ListEditor(getContext());
editor.applyList(listTitle, exclusive, repliesPolicy);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_edit_list_title)
.setIcon(R.drawable.ic_fluent_people_28_regular)
@@ -97,14 +98,15 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
.setPositiveButton(R.string.save, (d, which) -> {
String newTitle = editor.getTitle().trim();
setTitle(newTitle);
new UpdateList(listID, newTitle, editor.getRepliesPolicy()).setCallback(new Callback<>() {
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
if (getActivity() == null) return;
setTitle(list.title);
listTitle = list.title;
repliesPolicy = list.repliesPolicy;
E.post(new ListUpdatedCreatedEvent(listID, listTitle, repliesPolicy));
exclusive = list.exclusive;
E.post(new ListUpdatedCreatedEvent(listID, listTitle, exclusive, repliesPolicy));
}
@Override
@@ -127,7 +129,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override
protected TimelineDefinition makeTimelineDefinition() {
return TimelineDefinition.ofList(listID, listTitle);
return TimelineDefinition.ofList(listID, listTitle, exclusive);
}
@Override

View File

@@ -27,7 +27,7 @@ import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListTimelineEditor;
import org.joinmastodon.android.ui.views.ListEditor;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.ArrayList;
@@ -91,18 +91,18 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
ListEditor editor = new ListEditor(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<>() {
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
}
@Override
@@ -185,6 +185,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
item.exclusive = event.exclusive;
adapter.notifyItemChanged(i);
break;
}
@@ -242,7 +243,9 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
@@ -263,6 +266,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
args.putString("account", accountID);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
args.putBoolean("listIsExclusive", item.exclusive);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}

View File

@@ -14,6 +14,7 @@ public class ListTimeline extends BaseModel {
@RequiredField
public String title;
public RepliesPolicy repliesPolicy;
public boolean exclusive;
@NonNull
@Override
@@ -22,6 +23,7 @@ public class ListTimeline extends BaseModel {
"id='" + id + '\'' +
", title='" + title + '\'' +
", repliesPolicy=" + repliesPolicy +
", exclusive=" + exclusive +
'}';
}

View File

@@ -30,18 +30,20 @@ public class TimelineDefinition {
private @Nullable String listId;
private @Nullable String listTitle;
private boolean listIsExclusive;
private @Nullable String hashtagName;
public static TimelineDefinition ofList(String listId, String listTitle) {
public static TimelineDefinition ofList(String listId, String listTitle, boolean listIsExclusive) {
TimelineDefinition def = new TimelineDefinition(TimelineType.LIST);
def.listId = listId;
def.listTitle = listTitle;
def.listIsExclusive = listIsExclusive;
return def;
}
public static TimelineDefinition ofList(ListTimeline list) {
return ofList(list.id, list.title);
return ofList(list.id, list.title, list.exclusive);
}
public static TimelineDefinition ofHashtag(String hashtag) {
@@ -99,7 +101,7 @@ public class TimelineDefinition {
case LOCAL -> Icon.LOCAL;
case FEDERATED -> Icon.FEDERATED;
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
case LIST -> Icon.LIST;
case LIST -> listIsExclusive ? Icon.EXCLUSIVE_LIST : Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case BUBBLE -> Icon.BUBBLE;
};
@@ -155,6 +157,7 @@ public class TimelineDefinition {
def.title = title;
def.listId = listId;
def.listTitle = listTitle;
def.listIsExclusive = listIsExclusive;
def.hashtagName = hashtagName;
def.icon = icon == null ? null : Icon.values()[icon.ordinal()];
return def;
@@ -164,6 +167,7 @@ public class TimelineDefinition {
if (type == TimelineType.LIST) {
args.putString("listTitle", title);
args.putString("listID", listId);
args.putBoolean("listIsExclusive", listIsExclusive);
} else if (type == TimelineType.HASHTAG) {
args.putString("hashtag", hashtagName);
}
@@ -179,6 +183,7 @@ public class TimelineDefinition {
CITY(R.drawable.ic_fluent_city_24_regular, R.string.sk_icon_city),
IMAGE(R.drawable.ic_fluent_image_24_regular, R.string.sk_icon_image),
NEWS(R.drawable.ic_fluent_news_24_regular, R.string.sk_icon_news),
FEED(R.drawable.ic_fluent_rss_24_regular, R.string.sk_icon_feed),
COLOR_PALETTE(R.drawable.ic_fluent_color_24_regular, R.string.sk_icon_color_palette),
CAT(R.drawable.ic_fluent_animal_cat_24_regular, R.string.sk_icon_cat),
DOG(R.drawable.ic_fluent_animal_dog_24_regular, R.string.sk_icon_dog),
@@ -233,6 +238,7 @@ public class TimelineDefinition {
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);

View File

@@ -9,6 +9,7 @@ import android.view.MenuItem;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.Switch;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -16,18 +17,20 @@ import androidx.annotation.Nullable;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.ListTimeline;
public class ListTimelineEditor extends LinearLayout {
public class ListEditor extends LinearLayout {
private ListTimeline.RepliesPolicy policy = null;
private final TextInputFrameLayout input;
private final Button button;
private final Switch exclusiveSwitch;
@SuppressLint("ClickableViewAccessibility")
public ListTimelineEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public ListEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(R.layout.list_timeline_editor, this);
button = findViewById(R.id.button);
input = findViewById(R.id.input);
exclusiveSwitch = findViewById(R.id.exclusive_checkbox);
PopupMenu popupMenu = new PopupMenu(context, button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.list_reply_policies);
@@ -36,12 +39,15 @@ public class ListTimelineEditor extends LinearLayout {
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
input.getEditText().setHint(context.getString(R.string.sk_list_name_hint));
findViewById(R.id.exclusive)
.setOnClickListener(v -> exclusiveSwitch.setChecked(!exclusiveSwitch.isChecked()));
setRepliesPolicy(ListTimeline.RepliesPolicy.LIST);
}
public void applyList(String title, @Nullable ListTimeline.RepliesPolicy policy) {
public void applyList(String title, boolean exclusive, @Nullable ListTimeline.RepliesPolicy policy) {
input.getEditText().setText(title);
exclusiveSwitch.setChecked(exclusive);
if (policy != null) setRepliesPolicy(policy);
}
@@ -53,6 +59,10 @@ public class ListTimelineEditor extends LinearLayout {
return policy;
}
public boolean isExclusive() {
return exclusiveSwitch.isChecked();
}
public void setRepliesPolicy(@NonNull ListTimeline.RepliesPolicy policy) {
this.policy = policy;
switch (policy) {
@@ -73,15 +83,15 @@ public class ListTimelineEditor extends LinearLayout {
return true;
}
public ListTimelineEditor(Context context, AttributeSet attrs, int defStyleAttr) {
public ListEditor(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ListTimelineEditor(Context context, AttributeSet attrs) {
public ListEditor(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ListTimelineEditor(Context context) {
public ListEditor(Context context) {
this(context, null);
}
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M6.75 7.5C6.345 7.5 6 7.183 6 6.778V6.723C6 6.33 6.305 6.002 6.698 6H6.75C12.963 6 18 11.037 18 17.25v0.052C17.998 17.695 17.67 18 17.277 18h-0.055c-0.405 0-0.722-0.345-0.722-0.75 0-5.385-4.365-9.75-9.75-9.75z" android:fillColor="@color/fluent_default_icon_tint"/>
<path android:pathData="M13.294 18c0.38 0 0.701-0.287 0.705-0.666L14 17.25C14 13.246 10.754 10 6.75 10H6.666C6.287 10.006 6 10.328 6 10.707v0.09C6 11.195 6.351 11.5 6.75 11.5c3.176 0 5.75 2.574 5.75 5.75 0 0.399 0.305 0.75 0.704 0.75h0.09zM9 16.5C9 17.328 8.328 18 7.5 18S6 17.328 6 16.5 6.672 15 7.5 15 9 15.672 9 16.5z" android:fillColor="@color/fluent_default_icon_tint"/>
<path android:pathData="M6.25 3C4.455 3 3 4.455 3 6.25v11.5C3 19.545 4.455 21 6.25 21h11.5c1.795 0 3.25-1.455 3.25-3.25V6.25C21 4.455 19.545 3 17.75 3H6.25zM4.5 6.25c0-0.966 0.784-1.75 1.75-1.75h11.5c0.966 0 1.75 0.784 1.75 1.75v11.5c0 0.966-0.784 1.75-1.75 1.75H6.25c-0.966 0-1.75-0.784-1.75-1.75V6.25z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -39,4 +39,53 @@
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/exclusive"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:minHeight="48dp"
android:gravity="center_vertical"
android:layoutDirection="locale">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginHorizontal="16dp"
android:importantForAccessibility="no"
android:tint="?android:textColorPrimary"
android:src="@drawable/ic_fluent_rss_24_regular"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:paddingVertical="8dp"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:text="@string/sk_list_exclusive_switch" />
<Switch
android:id="@+id/exclusive_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:duplicateParentState="true"
android:focusable="false"
android:clickable="false"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="8dp"
android:textSize="14sp"
android:text="@string/sk_list_exclusive_switch_explanation" />
</LinearLayout>

View File

@@ -227,6 +227,7 @@
<string name="sk_icon_human">Human</string>
<string name="sk_icon_globe">Globe</string>
<string name="sk_icon_pin">Pin</string>
<string name="sk_icon_feed">Feed</string>
<string name="sk_edit_timeline">Edit timeline</string>
<string name="sk_edit_timelines">Edit timelines</string>
<string name="sk_alt_button">ALT</string>
@@ -305,4 +306,7 @@
<string name="sk_settings_prefix_replies_never">nobody</string>
<string name="sk_settings_prefix_replies_to_others">others</string>
<string name="sk_settings_forward_report_default">“Forward report” switch default</string>
<string name="sk_exclusive_list">Exclusive list</string>
<string name="sk_list_exclusive_switch">Make list exclusive</string>
<string name="sk_list_exclusive_switch_explanation">Members of an exclusive list will not show up on your home timeline if your instance supports it.</string>
</resources>