Gate poll, pin, and reaction UX in terminated groups.

This commit is contained in:
Cody Henthorne
2026-03-26 13:26:16 -04:00
parent 43f19d14d8
commit 9702728c19
7 changed files with 59 additions and 16 deletions

View File

@@ -258,7 +258,7 @@ public final class MenuState {
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText && !hasGift && !hasPayment && !hasPoll)
.shouldShowDeleteAction(!hasInMemory && onlyContainsCompleteMessages(selectedParts))
.shouldShowReactions(!conversationRecipient.isReleaseNotes())
.shouldShowReactions(!conversationRecipient.isReleaseNotes() && !conversationRecipient.isInactiveGroup())
.shouldShowPaymentDetails(hasPayment)
.shouldShowPollTerminate(hasPollTerminate)
.shouldShowPinMessage(canPinMessage)

View File

@@ -3003,6 +3003,14 @@ class ConversationFragment :
return
}
if (conversationGroupViewModel.groupRecordSnapshot?.isTerminated == true) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.conversation_activity__group_action_not_allowed_group_ended)
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
return
}
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.Poll__end_poll_title))
.setMessage(getString(R.string.Poll__end_poll_body))
@@ -3434,7 +3442,7 @@ class ConversationFragment :
context ?: return
val reactionsTag = "REACTIONS"
if (parentFragmentManager.findFragmentByTag(reactionsTag) == null) {
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(childFragmentManager, reactionsTag)
ReactionsBottomSheetDialogFragment.create(messageId, isMms, conversationGroupViewModel.groupRecordSnapshot?.isTerminated == true).show(childFragmentManager, reactionsTag)
}
}
@@ -3590,6 +3598,13 @@ class ConversationFragment :
}
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) {
if (conversationGroupViewModel.groupRecordSnapshot?.isTerminated == true) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.conversation_activity__group_action_not_allowed_group_ended)
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
return
}
viewModel.toggleVote(poll, pollOption, isChecked)
}

View File

@@ -221,6 +221,13 @@ class ConversationRepository(
if (threadRecipient.isPushV2Group && threadRecipient.groupId.getOrNull()?.isV2 != true) {
Log.w(TAG, "Missing group id")
emitter.tryOnError(Exception("Poll terminate failed"))
return@create
}
if (threadRecipient.isPushV2Group && !SignalDatabase.groups.isActive(threadRecipient.requireGroupId())) {
Log.w(TAG, "Cannot end poll in terminated or inactive group")
emitter.tryOnError(Exception("Poll terminate failed"))
return@create
}
val message = OutgoingMessage.pollTerminateMessage(

View File

@@ -73,7 +73,7 @@ class ConversationGroupViewModel(
fun canEditGroupInfo(): Boolean {
val memberLevel = _memberLevel.value ?: return true
return memberLevel.groupTableMemberLevel == GroupTable.MemberLevel.ADMINISTRATOR || memberLevel.allMembersCanEditGroupInfo
return groupRecordSnapshot?.isActive == true && (memberLevel.groupTableMemberLevel == GroupTable.MemberLevel.ADMINISTRATOR || memberLevel.allMembersCanEditGroupInfo)
}
fun blockJoinRequests(recipient: Recipient): Single<GroupBlockJoinRequestResult> {

View File

@@ -23,6 +23,12 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
private ReactionViewPagerAdapter.EventListener listener = null;
private List<ReactionDetails> data = Collections.emptyList();
private final boolean isGroupTerminated;
ReactionRecipientsAdapter(boolean isGroupTerminated) {
this.isGroupTerminated = isGroupTerminated;
}
void setListener(ReactionViewPagerAdapter.EventListener listener) {
this.listener = listener;
}
@@ -42,7 +48,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(data.get(position), listener);
holder.bind(data.get(position), listener, isGroupTerminated);
}
@Override
@@ -68,7 +74,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
tapToRemoveText = itemView.findViewById(R.id.reactions_bottom_view_recipient_tap_to_remove_action_text);
}
void bind(@NonNull ReactionDetails reaction, ReactionViewPagerAdapter.EventListener listener) {
void bind(@NonNull ReactionDetails reaction, ReactionViewPagerAdapter.EventListener listener, boolean isGroupTerminated) {
this.emoji.setText(reaction.getDisplayEmoji());
if (reaction.getSender().isSelf()) {
@@ -76,8 +82,13 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
this.avatar.setAvatar(Glide.with(avatar), null, false);
this.badge.setBadge(null);
AvatarUtil.loadIconIntoImageView(reaction.getSender(), avatar);
itemView.setOnClickListener((view) -> listener.onClick());
tapToRemoveText.setVisibility(View.VISIBLE);
if (isGroupTerminated) {
itemView.setOnClickListener(null);
tapToRemoveText.setVisibility(View.GONE);
} else {
itemView.setOnClickListener((view) -> listener.onClick());
tapToRemoveText.setVisibility(View.VISIBLE);
}
} else {
this.recipient.setText(reaction.getSender().getDisplayName(itemView.getContext()));
this.avatar.setAvatar(Glide.with(avatar), reaction.getSender(), false);

View File

@@ -20,10 +20,12 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
private int selectedPosition = 0;
private final EventListener listener;
private final boolean isGroupTerminated;
protected ReactionViewPagerAdapter(@NonNull EventListener listener) {
protected ReactionViewPagerAdapter(@NonNull EventListener listener, boolean isGroupTerminated) {
super(new AlwaysChangedDiffUtil<>());
this.listener = listener;
this.listener = listener;
this.isGroupTerminated = isGroupTerminated;
}
@NonNull EmojiCount getEmojiCount(int position) {
@@ -38,7 +40,7 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler, parent, false));
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler, parent, false), isGroupTerminated);
}
@Override
@@ -68,12 +70,13 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
static class ViewHolder extends RecyclerView.ViewHolder {
private final RecyclerView recycler;
private final ReactionRecipientsAdapter adapter = new ReactionRecipientsAdapter();
private final ReactionRecipientsAdapter adapter;
public ViewHolder(@NonNull View itemView) {
public ViewHolder(@NonNull View itemView, boolean isGroupTerminated) {
super(itemView);
recycler = (RecyclerView) itemView;
adapter = new ReactionRecipientsAdapter(isGroupTerminated);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
@@ -83,8 +86,8 @@ class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPager
}
public void onBind(@NonNull EmojiCount emojiCount, EventListener listener) {
adapter.updateData(emojiCount.getReactions());
adapter.setListener(listener);
adapter.updateData(emojiCount.getReactions());
}
public void setSelected(int position) {

View File

@@ -33,8 +33,9 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
private static final String ARGS_MESSAGE_ID = "reactions.args.message.id";
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
private static final String ARGS_IS_GROUP_TERMINATED = "reactions.args.is.group.terminated";
private ViewPager2 recipientPagerView;
private ReactionViewPagerAdapter recipientsAdapter;
@@ -44,11 +45,16 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
private final LifecycleDisposable disposables = new LifecycleDisposable();
public static DialogFragment create(long messageId, boolean isMms) {
return create(messageId, isMms, false);
}
public static DialogFragment create(long messageId, boolean isMms, boolean isGroupTerminated) {
Bundle args = new Bundle();
DialogFragment fragment = new ReactionsBottomSheetDialogFragment();
args.putLong(ARGS_MESSAGE_ID, messageId);
args.putBoolean(ARGS_IS_MMS, isMms);
args.putBoolean(ARGS_IS_GROUP_TERMINATED, isGroupTerminated);
fragment.setArguments(args);
@@ -144,7 +150,8 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
}
private void setUpRecipientsRecyclerView() {
recipientsAdapter = new ReactionViewPagerAdapter(() -> viewModel.removeReactionEmoji());
boolean isGroupTerminated = requireArguments().getBoolean(ARGS_IS_GROUP_TERMINATED, false);
recipientsAdapter = new ReactionViewPagerAdapter(() -> viewModel.removeReactionEmoji(), isGroupTerminated);
recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override