From 9702728c1918f2f0f3d4652832bbf54e63c6a2f1 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 26 Mar 2026 13:26:16 -0400 Subject: [PATCH] Gate poll, pin, and reaction UX in terminated groups. --- .../securesms/conversation/MenuState.java | 2 +- .../conversation/v2/ConversationFragment.kt | 17 ++++++++++++++++- .../conversation/v2/ConversationRepository.kt | 7 +++++++ .../v2/groups/ConversationGroupViewModel.kt | 2 +- .../reactions/ReactionRecipientsAdapter.java | 19 +++++++++++++++---- .../reactions/ReactionViewPagerAdapter.java | 15 +++++++++------ .../ReactionsBottomSheetDialogFragment.java | 13 ++++++++++--- 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java index 01b34eebe1..c1fc01e238 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 3fbe7bd049..bf1df8aed8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index 67eca947ac..de4c343c32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt index 9a97f8c66d..5f473ef1b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/groups/ConversationGroupViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index 1495dbb75b..ea69027fd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -23,6 +23,12 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter 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 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java index fc5b49e59c..6bbcd5461e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java @@ -20,10 +20,12 @@ class ReactionViewPagerAdapter extends ListAdapter()); - this.listener = listener; + this.listener = listener; + this.isGroupTerminated = isGroupTerminated; } @NonNull EmojiCount getEmojiCount(int position) { @@ -38,7 +40,7 @@ class ReactionViewPagerAdapter extends ListAdapter viewModel.removeReactionEmoji()); + boolean isGroupTerminated = requireArguments().getBoolean(ARGS_IS_GROUP_TERMINATED, false); + recipientsAdapter = new ReactionViewPagerAdapter(() -> viewModel.removeReactionEmoji(), isGroupTerminated); recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override