diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6c167d6959..67935b33e9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3152,6 +3152,10 @@ "messageformat": "Support Center", "description": "Label for the support center link in conversation details" }, + "icu:ConversationDetails__GroupTerminatedBanner": { + "messageformat": "This group was ended", + "description": "Conversation Header banner in conversation details for terminated groups" + }, "icu:SafetyNumberNotification__viewSafetyNumber": { "messageformat": "View Safety Number", "description": "In conversation, safety number change notification, label for button to view safety number, opens safety number modal" @@ -5260,6 +5264,10 @@ "messageformat": "Your request to join has been sent to the group admin. You’ll be notified when they take action.", "description": "Shown in composition area when you've requested to join a group" }, + "icu:GroupV2--join--group-terminated": { + "messageformat": "Join by link failed, group has ended.", + "description": "Shown if you click a group link for a group which was terminated" + }, "icu:GroupV2--join--general-join-failure--title": { "messageformat": "Link Error", "description": "Shown if something went wrong when you try to join via a group link" @@ -5268,6 +5276,18 @@ "messageformat": "Couldn't join group. Try again later.", "description": "Shown if something went wrong when you try to join via a group link" }, + "icu:GroupV2--terminate-group-in-progress": { + "messageformat": "Ending group...", + "description": "Shown while waiting for a group terminate request to finish" + }, + "icu:TerminateGroupFailedModal__description": { + "messageformat": "Ending the group failed. Check your connection and try again.", + "description": "Shown when a group terminate request failed" + }, + "icu:TerminateGroupFailedModal__try-again": { + "messageformat": "Try again", + "description": "Shown when a group terminate request failed" + }, "icu:GroupV2--admin": { "messageformat": "Admin", "description": "Label for a group administrator" @@ -5856,6 +5876,18 @@ "messageformat": "This group's members or settings have changed.", "description": "When rejoining a group, any detected changes are collapsed down into this summary" }, + "icu:GroupV2--terminated--you": { + "messageformat": "You ended the group", + "description": "Shown in timeline or conversation preview when group is terminated by you" + }, + "icu:GroupV2--terminated--other": { + "messageformat": "{memberName} ended the group", + "description": "Shown in timeline or conversation preview when group is terminated by someone else" + }, + "icu:GroupV2--terminated--unknown": { + "messageformat": "This group was ended", + "description": "Shown in timeline or conversation preview when group is terminated" + }, "icu:GroupV1--Migration--disabled--link": { "messageformat": "Upgrade this group to activate new features like @mentions and admins. Members who have not shared their name or photo in this group will be invited to join. Learn more.", "description": "Shown instead of composition area when user is forced to migrate a legacy group (GV1)." @@ -6224,6 +6256,10 @@ "messageformat": "Replace", "description": "Composition Area > GIF Picker > After selecting a GIF > When you already have an attachment > Confirm Dialog > Replace Button" }, + "icu:CompositionArea--group-terminated": { + "messageformat": "You can’t send messages because the group was ended.", + "description": "Shown instead of message composer when group is terminated" + }, "icu:CompositionArea--viewOnceToggle": { "messageformat": "View once", "description": "Aria label for the view once toggle button in the composition input" @@ -6460,6 +6496,10 @@ "messageformat": "Leave group", "description": "This is a button to leave a group" }, + "icu:ConversationDetailsActions--terminate-group": { + "messageformat": "End group", + "description": "This is a button to terminate a group permanently" + }, "icu:ConversationDetailsActions--block-group": { "messageformat": "Block group", "description": "This is a button to block a group" @@ -6468,6 +6508,18 @@ "messageformat": "Unblock group", "description": "This is a button to unblock a group" }, + "icu:ConversationDetailsActions--archive": { + "messageformat": "Archive chat", + "description": "This is a button to archive a chat" + }, + "icu:ConversationDetailsActions--unarchive": { + "messageformat": "Unarchive chat", + "description": "This is a button to unarchive a chat" + }, + "icu:ConversationDetailsActions--delete": { + "messageformat": "Delete chat", + "description": "This is a button to delete a chat" + }, "icu:ConversationDetailsActions--leave-group-must-choose-new-admin": { "messageformat": "Before you leave, you must choose at least one new admin for this group.", "description": "Shown if, before leaving a group, you need to choose an admin" @@ -6484,6 +6536,22 @@ "messageformat": "Leave", "description": "This is the modal button to confirm leaving a group" }, + "icu:ConversationDetailsActions--prompt-terminate-group-modal-title": { + "messageformat": "End \"{groupName}\"?", + "description": "This is the modal title for confirming terminating a group" + }, + "icu:ConversationDetailsActions--prompt-terminate-group-modal-content": { + "messageformat": "Members will no longer be able to send messages or start calls in the group. They will be notified that you ended the group, and will still have access to message history.", + "description": "This is the modal content for terminating a group. After this modal, a second confirmation modal will show." + }, + "icu:ConversationDetailsActions--terminate-group-modal-confirm": { + "messageformat": "End group", + "description": "This is the modal button to confirm terminating a group" + }, + "icu:ConversationDetailsActions--confirm-terminate-group-confirm-modal-content": { + "messageformat": "This will end the group permanently. Are you sure you want to proceed?", + "description": "This is the modal content for confirming terminating a group" + }, "icu:ConversationDetailsActions--unblock-group-modal-title": { "messageformat": "Unblock the \"{groupName}\" Group?", "description": "This is the modal title for confirming unblock of a group" @@ -6520,6 +6588,10 @@ "messageformat": "{number, plural, one {# member} other {# members}}", "description": "The title of the membership list panel" }, + "icu:ConversationDetailsMembershipList--terminated-title": { + "messageformat": "{number, plural, one {# former member} other {# former members}}", + "description": "The title of the membership list panel for a terminated group" + }, "icu:ConversationDetailsMembershipList--add-members": { "messageformat": "Add members", "description": "The button that you can click to add new members" @@ -8924,6 +8996,10 @@ "messageformat": "Delete this story? It will also be deleted for everyone who received it.", "description": "Confirmation dialog description text for deleting a story" }, + "icu:MyStories__delete-group-story-for-me": { + "messageformat": "Delete this story? It will only be deleted for you because the group has ended.", + "description": "Confirmation dialog description text for deleting a story" + }, "icu:payment-event-notification-message-you-label": { "messageformat": "You started a payment to {receiver}", "description": "Payment event notification from you message bubble label" diff --git a/images/icons/v3/group/group-x-inline.svg b/images/icons/v3/group/group-x-inline.svg new file mode 100644 index 0000000000..39b0ce7d9e --- /dev/null +++ b/images/icons/v3/group/group-x-inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/protos/Backups.proto b/protos/Backups.proto index 1038bdab04..b1b17f655a 100644 --- a/protos/Backups.proto +++ b/protos/Backups.proto @@ -312,6 +312,7 @@ message Group { bytes inviteLinkPassword = 10; bool announcements_only = 12; repeated MemberBanned members_banned = 13; + bool terminated = 14; } message GroupAttributeBlob { @@ -1053,7 +1054,6 @@ message GroupChangeChatUpdate { GroupAvatarUpdate groupAvatarUpdate = 4; GroupDescriptionUpdate groupDescriptionUpdate = 5; GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6; - GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35; GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7; GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8; GroupAdminStatusUpdate groupAdminStatusUpdate = 9; @@ -1082,7 +1082,9 @@ message GroupChangeChatUpdate { GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32; GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33; GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34; - // next: 36 + GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35; + GroupTerminateChangeUpdate groupTerminateChangeUpdate = 36; + // next: 37 } } @@ -1297,6 +1299,10 @@ message GroupExpirationTimerUpdate { optional bytes updaterAci = 2; } +message GroupTerminateChangeUpdate { + optional bytes updaterAci = 1; +} + message PollTerminateUpdate { uint64 targetSentTimestamp = 1; string question = 2; // Between 1-100 characters diff --git a/protos/Groups.proto b/protos/Groups.proto index 6415509d50..d9f085719e 100644 --- a/protos/Groups.proto +++ b/protos/Groups.proto @@ -86,7 +86,8 @@ message Group { bytes inviteLinkPassword = 10; bool announcements_only = 12; repeated MemberBanned members_banned = 13; - // next: 14 + bool terminated = 14; + // next: 15 } message GroupAttributeBlob { @@ -237,6 +238,8 @@ message GroupChange { bool announcements_only = 1; } + message TerminateGroupAction {} + bytes sourceUserId = 1; // clients should not provide this value; the server will provide it in the response buffer to ensure the signature is binding to a particular group // if clients set it during a request the server will respond with 400. @@ -246,7 +249,7 @@ message GroupChange { repeated AddMemberAction addMembers = 3; repeated DeleteMemberAction deleteMembers = 4; repeated ModifyMemberRoleAction modifyMemberRoles = 5; - repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6; + repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6 repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6; repeated AddMemberPendingProfileKeyAction addMembersPendingProfileKey = 7; repeated DeleteMemberPendingProfileKeyAction deleteMembersPendingProfileKey = 8; @@ -267,7 +270,8 @@ message GroupChange { repeated AddMemberBannedAction add_members_banned = 22; // change epoch = 4 repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4 repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5 - // next: 28 + TerminateGroupAction terminate_group = 28; // change epoch = 7 + // next: 29 } bytes actions = 1; diff --git a/stylesheets/components/ConversationDetails.scss b/stylesheets/components/ConversationDetails.scss index 277eed7fdf..e6dd7e799e 100644 --- a/stylesheets/components/ConversationDetails.scss +++ b/stylesheets/components/ConversationDetails.scss @@ -88,9 +88,12 @@ } } - &__block-group { + &__block-group, + &__terminate-group, + &__delete { color: variables.$color-accent-red; } + &__unblock-group { color: variables.$color-accent-blue; } @@ -192,6 +195,12 @@ } } + &--archive { + &::after { + @include details-icon('../images/icons/v3/archive/archive.svg'); + } + } + &--approve { &::after { @include details-icon('../images/icons/v3/check/check.svg'); @@ -236,6 +245,16 @@ } } + &--delete { + &::after { + @include details-icon( + '../images/icons/v3/trash/trash.svg', + variables.$color-accent-red, + variables.$color-accent-red + ); + } + } + &--invites { &::after { @include details-icon('../images/icons/v3/group/group.svg'); @@ -338,6 +357,16 @@ opacity: 0.5; } } + + &--terminate { + &::after { + @include details-icon( + '../images/icons/v3/x/x-circle.svg', + variables.$color-accent-red, + variables.$color-accent-red + ); + } + } } } diff --git a/stylesheets/components/SystemMessage.scss b/stylesheets/components/SystemMessage.scss index e127252699..dd3956e78d 100644 --- a/stylesheets/components/SystemMessage.scss +++ b/stylesheets/components/SystemMessage.scss @@ -143,6 +143,15 @@ @include system-message-icon('../images/icons/v3/info/info-compact.svg'); } + &--icon-group-terminate::before { + @include system-message-icon( + '../images/icons/v3/group/group-x-inline.svg' + ); + & { + width: 24px; + } + } + &--icon-info::before { @include system-message-icon('../images/icons/v3/info/info-compact.svg'); } diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index fd11b354e2..1e183b33e8 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -38,6 +38,8 @@ const SemverKeys = [ 'desktop.binaryServiceId.prod', 'desktop.groupMemberLabels.edit.beta', 'desktop.groupMemberLabels.edit.prod', + 'desktop.groupTerminate.send.beta', + 'desktop.groupTerminate.send.prod', 'desktop.keyTransparency.beta', 'desktop.keyTransparency.prod', 'desktop.localBackups.beta', diff --git a/ts/background.preload.ts b/ts/background.preload.ts index 083c28ff77..1bd564005c 100644 --- a/ts/background.preload.ts +++ b/ts/background.preload.ts @@ -2242,6 +2242,13 @@ export async function startApp(): Promise { } } + if (conversation?.get('terminated')) { + log.info( + `onTyping: conversation ${conversation.idForLogging()} is terminated group, dropping typing message` + ); + return; + } + if (conversation?.isBlocked()) { log.info( `onTyping: conversation ${conversation.idForLogging()} is blocked, dropping typing message` diff --git a/ts/components/CompositionArea.dom.stories.tsx b/ts/components/CompositionArea.dom.stories.tsx index d89227785a..590db812ab 100644 --- a/ts/components/CompositionArea.dom.stories.tsx +++ b/ts/components/CompositionArea.dom.stories.tsx @@ -72,6 +72,7 @@ export default { }, announcementsOnly: { control: { type: 'boolean' } }, areWePendingApproval: { control: { type: 'boolean' } }, + terminated: { control: { type: 'boolean' } }, }, args: { acceptedMessageRequest: true, @@ -145,6 +146,7 @@ export default { announcementsOnly: false, areWeAdmin: false, areWePendingApproval: false, + terminated: false, groupAdmins, memberColors, cancelJoinRequest: action('cancelJoinRequest'), @@ -259,6 +261,13 @@ export function AnnouncementsOnlyGroup(args: Props): React.JSX.Element { ); } +export function TerminatedGroup(args: Props): React.JSX.Element { + const theme = useContext(StorybookThemeContext); + return ( + + ); +} + export function Quote(args: Props): React.JSX.Element { const theme = useContext(StorybookThemeContext); return ( diff --git a/ts/components/CompositionArea.dom.tsx b/ts/components/CompositionArea.dom.tsx index d27980ede4..24ca7d2609 100644 --- a/ts/components/CompositionArea.dom.tsx +++ b/ts/components/CompositionArea.dom.tsx @@ -214,6 +214,7 @@ export type OwnProps = Readonly<{ shouldSendHighQualityAttachments: boolean; showConversation: ShowConversationType; startRecording: (id: string) => unknown; + terminated: boolean | null; theme: ThemeType; renderSmartCompositionRecording: () => React.JSX.Element; renderSmartCompositionRecordingDraft: ( @@ -348,6 +349,7 @@ export const CompositionArea = memo(function CompositionArea({ areWeAdmin, groupAdmins, memberColors, + terminated, cancelJoinRequest, showConversation, // SMS-only contacts @@ -984,6 +986,19 @@ export const CompositionArea = memo(function CompositionArea({ ); } + if (terminated) { + return ( +
+ {i18n('icu:CompositionArea--group-terminated')} +
+ ); + } + if ( isBlocked || areWePending || diff --git a/ts/components/DisappearingTimerSelect.dom.tsx b/ts/components/DisappearingTimerSelect.dom.tsx index 808a01d579..1cdecb2f55 100644 --- a/ts/components/DisappearingTimerSelect.dom.tsx +++ b/ts/components/DisappearingTimerSelect.dom.tsx @@ -13,12 +13,13 @@ import { tw } from '../axo/tw.dom.js'; export type Props = { i18n: LocalizerType; + disabled?: boolean; value?: DurationInSeconds; onChange(value: DurationInSeconds): void; }; export function DisappearingTimerSelect(props: Props): React.JSX.Element { - const { i18n, value = DurationInSeconds.ZERO, onChange } = props; + const { i18n, disabled, value = DurationInSeconds.ZERO, onChange } = props; const [isModalOpen, setIsModalOpen] = useState(false); @@ -99,6 +100,7 @@ export function DisappearingTimerSelect(props: Props): React.JSX.Element { return (
diff --git a/ts/components/GlobalModalContainer.dom.tsx b/ts/components/GlobalModalContainer.dom.tsx index c6a06f4d88..a598beedde 100644 --- a/ts/components/GlobalModalContainer.dom.tsx +++ b/ts/components/GlobalModalContainer.dom.tsx @@ -190,6 +190,9 @@ export type PropsType = { // LocalBackupExportWorkflow shouldShowLocalBackupExportWorkflow: boolean; renderLocalBackupExportWorkflow: () => React.JSX.Element; + // TerminateGroupFailedModal + terminateGroupFailedModal: { conversationId: string } | null; + renderTerminateGroupFailedModal: () => React.JSX.Element | null; }; export function GlobalModalContainer({ @@ -314,6 +317,9 @@ export function GlobalModalContainer({ // LocalBackupExportWorkflow shouldShowLocalBackupExportWorkflow, renderLocalBackupExportWorkflow, + // TerminateGroupFailedModal + terminateGroupFailedModal, + renderTerminateGroupFailedModal, }: PropsType): React.JSX.Element | null { // We want the following dialogs to show in this order: // 0. Stateful multi-modal workflows @@ -565,5 +571,9 @@ export function GlobalModalContainer({ ); } + if (terminateGroupFailedModal) { + return renderTerminateGroupFailedModal(); + } + return null; } diff --git a/ts/components/ProgressDialog.dom.tsx b/ts/components/ProgressDialog.dom.tsx index 6f86359956..d38cd27a0e 100644 --- a/ts/components/ProgressDialog.dom.tsx +++ b/ts/components/ProgressDialog.dom.tsx @@ -7,10 +7,12 @@ import { Spinner } from './Spinner.dom.js'; export type PropsType = { readonly i18n: LocalizerType; + readonly description?: string; }; // TODO: This should use . See DESKTOP-1038. export const ProgressDialog = React.memo(function ProgressDialogInner({ + description, i18n, }: PropsType) { return ( @@ -18,7 +20,9 @@ export const ProgressDialog = React.memo(function ProgressDialogInner({
-
{i18n('icu:updating')}
+
+ {description ?? i18n('icu:updating')} +
); }); diff --git a/ts/components/ProgressModal.dom.tsx b/ts/components/ProgressModal.dom.tsx index 238c7e9f41..79b411dd5c 100644 --- a/ts/components/ProgressModal.dom.tsx +++ b/ts/components/ProgressModal.dom.tsx @@ -8,9 +8,11 @@ import type { LocalizerType } from '../types/Util.std.js'; export type PropsType = { readonly i18n: LocalizerType; + readonly description?: string; }; export const ProgressModal = React.memo(function ProgressModalInner({ + description, i18n, }: PropsType) { const [root, setRoot] = React.useState(null); @@ -32,7 +34,7 @@ export const ProgressModal = React.memo(function ProgressModalInner({ return root ? createPortal(
- +
, root ) diff --git a/ts/components/SendStoryModal.dom.tsx b/ts/components/SendStoryModal.dom.tsx index 790930d18c..1211984c30 100644 --- a/ts/components/SendStoryModal.dom.tsx +++ b/ts/components/SendStoryModal.dom.tsx @@ -49,6 +49,7 @@ import { } from '../types/VisualAttachment.dom.js'; import { UserText } from './UserText.dom.js'; import { Theme } from '../util/theme.std.js'; +import { strictAssert } from '../util/assert.std.js'; const { noop, sortBy } = lodash; @@ -183,6 +184,11 @@ export function SendStoryModal({ [distributionLists, groupStories, selectedGroupIds, selectedListIds, i18n] ); + const nonTerminatedGroupStories = useMemo( + () => groupStories.filter(group => !group.terminated), + [groupStories] + ); + const [searchTerm, setSearchTerm] = useState(''); const [filteredConversations, setFilteredConversations] = useState( @@ -629,7 +635,7 @@ export function SendStoryModal({ // my stories always first, the rest sorted by recency const fullList = sortBy( - [...groupStories, ...distributionLists], + [...nonTerminatedGroupStories, ...distributionLists], listOrGroup => { if (listOrGroup.id === MY_STORY_ID) { return Number.NEGATIVE_INFINITY; @@ -790,6 +796,11 @@ export function SendStoryModal({ return; } + strictAssert( + !group.terminated, + "Can't send story to terminated group" + ); + setSelectedGroupIds(groupIds => { if (value) { groupIds.add(group.id); diff --git a/ts/components/StoryViewer.dom.tsx b/ts/components/StoryViewer.dom.tsx index 1f3ebfaa60..0172ccab89 100644 --- a/ts/components/StoryViewer.dom.tsx +++ b/ts/components/StoryViewer.dom.tsx @@ -79,6 +79,7 @@ export type PropsType = { | 'sortedGroupMembers' | 'title' | 'left' + | 'terminated' >; hasActiveCall?: boolean; hasAllStoriesUnmuted: boolean; @@ -483,6 +484,7 @@ export function StoryViewer({ | undefined; if (isSent) { + // TODO: DESKTOP-9943 contextMenuOptions = [ { icon: 'StoryListItem__icon--info', @@ -1001,7 +1003,9 @@ export function StoryViewer({ i18n={i18n} onClose={() => setConfirmDeleteStory(undefined)} > - {i18n('icu:MyStories__delete')} + {group?.terminated + ? i18n('icu:MyStories__delete-group-story-for-me') + : i18n('icu:MyStories__delete')} )} diff --git a/ts/components/TerminateGroupFailedModal.dom.stories.tsx b/ts/components/TerminateGroupFailedModal.dom.stories.tsx new file mode 100644 index 0000000000..a08dd85ae4 --- /dev/null +++ b/ts/components/TerminateGroupFailedModal.dom.stories.tsx @@ -0,0 +1,30 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Meta, StoryFn } from '@storybook/react'; +import React, { type ComponentProps } from 'react'; + +import { action } from '@storybook/addon-actions'; +import { TerminateGroupFailedModal } from './TerminateGroupFailedModal.dom.js'; + +const { i18n } = window.SignalContext; + +type PropsType = ComponentProps; + +export default { + title: 'Components/TerminateGroupFailedModal', + component: TerminateGroupFailedModal, + args: { + i18n, + onClose: action('close'), + onRetry: action('retry'), + }, +} satisfies Meta; + +// eslint-disable-next-line react/function-component-definition +const Template: StoryFn = args => ( + +); + +export const Modal = Template.bind({}); +Modal.args = {}; diff --git a/ts/components/TerminateGroupFailedModal.dom.tsx b/ts/components/TerminateGroupFailedModal.dom.tsx new file mode 100644 index 0000000000..1bd04bc2e7 --- /dev/null +++ b/ts/components/TerminateGroupFailedModal.dom.tsx @@ -0,0 +1,42 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useCallback } from 'react'; +import type { LocalizerType } from '../types/Util.std.js'; +import { AxoAlertDialog } from '../axo/AxoAlertDialog.dom.js'; + +export type PropsType = Readonly<{ + i18n: LocalizerType; + onClose: () => void; + onRetry: () => void; +}>; + +export function TerminateGroupFailedModal(props: PropsType): React.JSX.Element { + const { i18n, onClose, onRetry } = props; + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open) { + onClose(); + } + }, + [onClose] + ); + + return ( + + + + + {i18n('icu:TerminateGroupFailedModal__description')} + + + + {i18n('icu:cancel')} + + {i18n('icu:TerminateGroupFailedModal__try-again')} + + + + + ); +} diff --git a/ts/components/conversation/ConversationHeader.dom.tsx b/ts/components/conversation/ConversationHeader.dom.tsx index b9a820f3aa..ef2e730235 100644 --- a/ts/components/conversation/ConversationHeader.dom.tsx +++ b/ts/components/conversation/ConversationHeader.dom.tsx @@ -257,6 +257,8 @@ export const ConversationHeader = memo(function ConversationHeader({ MessageRequestState.default ); + const isTerminated = Boolean(conversation.terminated); + if (hasPanelShowing) { return null; } @@ -343,17 +345,19 @@ export const ConversationHeader = memo(function ConversationHeader({ onViewConversationDetails={onViewConversationDetails} isSignalConversation={isSignalConversation ?? false} /> - {!isSmsOnlyOrUnregistered && !isSignalConversation && ( - - )} + {!isSmsOnlyOrUnregistered && + !isSignalConversation && + !isTerminated && ( + + )}