From efb7c6725c24b6fe35a7561574c9987e6de31bcd Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:22:47 -0600 Subject: [PATCH] Admin Delete Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com> --- _locales/en/messages.json | 44 ++ protos/Backups.proto | 6 + protos/SignalService.proto | 8 +- protos/SignalStorage.proto | 1 + stylesheets/_modules.scss | 12 +- stylesheets/components/ContactName.scss | 2 +- stylesheets/tailwind-config.css | 1 + .../container-scroll-state.css | 16 + ts/RemoteConfig.dom.ts | 6 + ts/axo/AxoAlertDialog.dom.tsx | 39 +- ts/axo/AxoButton.dom.tsx | 5 + ts/axo/_internal/FlexWrapDetector.dom.tsx | 31 + ts/background.preload.ts | 112 +++- .../DeleteMessagesModal.dom.stories.tsx | 122 ++-- ts/components/DeleteMessagesModal.dom.tsx | 272 +++++++-- .../EditHistoryMessagesModal.dom.tsx | 2 + ts/components/StoryViewsNRepliesModal.dom.tsx | 2 + ts/components/conversation/Message.dom.tsx | 152 +++-- .../conversation/MessageAudio.dom.tsx | 2 + .../conversation/MessageBody.dom.tsx | 2 +- .../MessageDetail.dom.stories.tsx | 1 + .../conversation/MessageDetail.dom.tsx | 7 +- .../conversation/MessageMetadata.dom.tsx | 67 ++- .../TimelineMessage.dom.stories.tsx | 210 ++++++- .../GroupMemberLabelEditor.dom.tsx | 2 + .../ConversationListItem.dom.tsx | 16 +- ts/jobs/conversationJobQueue.preload.ts | 4 +- .../helpers/sendDeleteForEveryone.preload.ts | 39 +- .../sendDeleteStoryForEveryone.preload.ts | 9 +- ts/jobs/helpers/sendNormalMessage.preload.ts | 6 - ts/jobs/helpers/sendNullMessage.preload.ts | 2 +- ts/messageModifiers/Deletes.preload.ts | 30 +- ts/messages/handleDataMessage.preload.ts | 6 +- ts/model-types.d.ts | 4 + ts/models/conversations.preload.ts | 12 +- ts/services/backups/export.preload.ts | 13 +- ts/services/backups/import.preload.ts | 30 + ts/services/storageRecordOps.preload.ts | 13 + ts/state/ducks/conversations.preload.ts | 47 +- ts/state/ducks/items.preload.ts | 10 + ts/state/selectors/items.dom.ts | 6 + ts/state/selectors/message.preload.ts | 569 +++++++++++++----- .../smart/DeleteMessagesModal.preload.tsx | 28 +- ts/state/smart/MessageDetail.preload.tsx | 2 + .../state/selectors/messages_test.preload.ts | 245 +++++--- ts/test-helpers/generateBackup.node.ts | 1 + ts/test-helpers/getDefaultConversation.std.ts | 9 +- ts/textsecure/SendMessage.preload.ts | 33 +- ts/textsecure/Types.d.ts | 6 + ts/textsecure/processDataMessage.preload.ts | 21 + ts/types/Message.std.ts | 1 + ts/types/Storage.d.ts | 1 + ts/util/canDeleteForEveryone.preload.ts | 266 ++++++++ ts/util/cleanup.preload.ts | 2 +- ts/util/deleteForEveryone.preload.ts | 109 ++-- ...eleteGroupStoryReplyForEveryone.preload.ts | 2 - ts/util/deleteStoryForEveryone.preload.ts | 1 - ts/util/getDeleteMaxAgeMs.dom.ts | 26 + ts/util/getLastMessage.preload.ts | 62 +- ts/util/getMessageAge.std.ts | 12 + ts/util/getMessageAuthorText.preload.ts | 19 + ts/util/handleEditMessage.preload.ts | 4 +- ts/util/isAdminDeleteEnabled.dom.ts | 18 + ...ge.std.ts => isTooOldToEditMessage.std.ts} | 9 +- ts/util/modifyTargetMessage.preload.ts | 6 +- ts/util/onStoryRecipientUpdate.preload.ts | 11 +- .../sendDeleteForEveryoneMessage.preload.ts | 63 +- 67 files changed, 2328 insertions(+), 569 deletions(-) create mode 100644 stylesheets/tailwind-plugins/container-scroll-state.css create mode 100644 ts/axo/_internal/FlexWrapDetector.dom.tsx create mode 100644 ts/util/canDeleteForEveryone.preload.ts create mode 100644 ts/util/getDeleteMaxAgeMs.dom.ts create mode 100644 ts/util/getMessageAge.std.ts create mode 100644 ts/util/isAdminDeleteEnabled.dom.ts rename ts/util/{isTooOldToModifyMessage.std.ts => isTooOldToEditMessage.std.ts} (53%) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8b3b1d295c..91898fcc26 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2948,6 +2948,22 @@ "messageformat": "Delete failed", "description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to anyone" }, + "icu:deleteFailedClickForDetails": { + "messageformat": "Delete failed, click for details", + "description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to anyone and can be retried" + }, + "icu:retryDeleteForEveryone--title": { + "messageformat": "Retry failed delete?", + "description": "Title of dialog shown when user clicks on a failed delete for everyone message to retry (screen reader only)" + }, + "icu:retryDeleteForEveryone--body": { + "messageformat": "Message failed to delete. Check your connection and try again.", + "description": "Body text of dialog shown when user clicks on a failed delete for everyone message to retry" + }, + "icu:retryDeleteForEveryone--tryAgain": { + "messageformat": "Try again", + "description": "Button text to retry a failed delete for everyone" + }, "icu:editFailed": { "messageformat": "Edit failed, click for details", "description": "Shown on a message which was edited if the edit wasn't successfully sent to anyone" @@ -2964,6 +2980,10 @@ "messageformat": "Partially deleted, click to retry", "description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to everyone" }, + "icu:partiallyDeleted--clickForDetails": { + "messageformat": "Partially deleted, click for details", + "description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to everyone" + }, "icu:expiredWarning": { "messageformat": "This version of Signal Desktop has expired. Please upgrade to the latest version to continue messaging.", "description": "Warning notification that this version of the app has expired" @@ -3484,6 +3504,18 @@ "messageformat": "This message was deleted.", "description": "Shown in a message's bubble when the message has been deleted for everyone." }, + "icu:message--deletedByAdmin": { + "messageformat": "Admin {admin} deleted this message", + "description": "Shown in a message's bubble when the message has been deleted by a group admin." + }, + "icu:message--deletedForEveryone--incoming": { + "messageformat": "{name} deleted this message", + "description": "Shown in a message bubble when an incoming message was deleted for everyone by its author." + }, + "icu:message--deletedForEveryone--outgoing": { + "messageformat": "You deleted this message", + "description": "Shown in a message bubble when you deleted your own message for everyone." + }, "icu:message--attachmentTooBig--one": { "messageformat": "Attachment too large to display.", "description": "Shown in a message bubble if no attachments are left on message when too-large attachments are dropped" @@ -6640,6 +6672,10 @@ "messageformat": "Delete {count, plural, one {message} other {# messages}}?", "description": "delete selected messages > confirmation modal > title" }, + "icu:DeleteMessagesModal--title-2": { + "messageformat": "Delete selected {count, plural, one {message} other {# messages}}?", + "description": "delete selected messages > confirmation modal > title" + }, "icu:DeleteMessagesModal--description": { "messageformat": "Who would you like to delete {count, plural, one {this message} other {these messages}} for?", "description": "delete selected messages > confirmation modal > description" @@ -6664,6 +6700,14 @@ "messageformat": "Delete for everyone", "description": "delete selected messages > confirmation modal > delete for everyone" }, + "icu:DeleteMessagesModal--adminDeleteConfirmation--title": { + "messageformat": "Delete for everyone?", + "description": "delete selected messages > admin delete confirmation modal > title" + }, + "icu:DeleteMessagesModal--adminDeleteConfirmation--description": { + "messageformat": "As an admin, group members will see that you deleted {count, plural, one {this message} other {these messages}}.", + "description": "delete selected messages > admin delete confirmation modal > description" + }, "icu:DeleteMessagesModal--deleteFromAllDevices": { "messageformat": "Delete from all devices", "description": "within note to self conversation > delete selected messages > confirmation modal > delete from all devices (same as delete for everyone)" diff --git a/protos/Backups.proto b/protos/Backups.proto index 6345fdafe1..07c9286402 100644 --- a/protos/Backups.proto +++ b/protos/Backups.proto @@ -137,6 +137,7 @@ message AccountData { CallsUseLessDataSetting callsUseLessDataSetting = 29; // If unset, treat the same as "Unknown" case bool allowSealedSenderFromAnyone = 30; bool allowAutomaticKeyVerification = 31; + bool hasSeenAdminDeleteEducationDialog = 32; } message SubscriberData { @@ -504,6 +505,7 @@ message ChatItem { ViewOnceMessage viewOnceMessage = 18; DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up Poll poll = 20; + AdminDeletedMessage adminDeletedMessage = 22; } PinDetails pinDetails = 21; // only set if message is pinned @@ -904,6 +906,10 @@ message Poll { repeated Reaction reactions = 5; } +message AdminDeletedMessage { + uint64 adminId = 1; // id of the admin that deleted the message +} + message ChatUpdateMessage { // If unset, importers should ignore the update message without throwing an error. oneof update { diff --git a/protos/SignalService.proto b/protos/SignalService.proto index dc5b4688a8..e43df52531 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -404,6 +404,11 @@ message DataMessage { optional uint64 targetSentTimestamp = 2; } + message AdminDelete { + optional bytes targetAuthorAciBinary = 1; // 16-byte UUID + optional uint64 targetSentTimestamp = 2; + } + optional string body = 1; repeated AttachmentPointer attachments = 2; reserved /*groupV1*/ 3; @@ -431,7 +436,8 @@ message DataMessage { optional PollVote pollVote = 26; optional PinMessage pinMessage = 27; optional UnpinMessage unpinMessage = 28; - // NEXT ID: 29 + optional AdminDelete adminDelete = 29; + // NEXT ID: 30 } message NullMessage { diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index 3c3c20c602..45258729da 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -302,6 +302,7 @@ message AccountRecord { NotificationProfileManualOverride notificationProfileManualOverride = 44; bool notificationProfileSyncDisabled = 45; bool automaticKeyVerificationDisabled = 46; + bool hasSeenAdminDeleteEducationDialog = 47; } message StoryDistributionListRecord { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e3b42c1a92..096968af9f 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1181,13 +1181,7 @@ $message-padding-horizontal: 12px; } .module-message__text--delete-for-everyone { user-select: none; - - @include mixins.light-theme { - color: variables.$color-gray-90; - } - @include mixins.dark-theme { - color: variables.$color-gray-05; - } + color: light-dark(variables.$color-gray-60, variables.$color-gray-25); } .module-message__metadata { @@ -6322,10 +6316,6 @@ button.module-calling-participants-list__contact { &--with-reactions { margin-bottom: -6px; } - - &--deleted-for-everyone { - font-style: italic; - } } .module-message__container--sticker-like { diff --git a/stylesheets/components/ContactName.scss b/stylesheets/components/ContactName.scss index e412cf6c65..9ee78d5c7c 100644 --- a/stylesheets/components/ContactName.scss +++ b/stylesheets/components/ContactName.scss @@ -7,7 +7,7 @@ @use '../variables'; @use '../mixins'; -button.module-contact-name { +:where(button).module-contact-name { @include mixins.button-reset; &:hover, diff --git a/stylesheets/tailwind-config.css b/stylesheets/tailwind-config.css index 20727e1a23..783d3dc2cb 100644 --- a/stylesheets/tailwind-config.css +++ b/stylesheets/tailwind-config.css @@ -6,6 +6,7 @@ @import 'tailwindcss' source(none); @import './tailwind-plugins/animate-general.css'; @import './tailwind-plugins/animate-enter-exit.css'; +@import './tailwind-plugins/container-scroll-state.css'; @import './tailwind-plugins/scrollbar.css'; @import './tailwind-plugins/superellipse.css'; diff --git a/stylesheets/tailwind-plugins/container-scroll-state.css b/stylesheets/tailwind-plugins/container-scroll-state.css new file mode 100644 index 0000000000..fcee0bbbae --- /dev/null +++ b/stylesheets/tailwind-plugins/container-scroll-state.css @@ -0,0 +1,16 @@ +/** + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +@custom-variant container-scrollable { + @container not scroll-state(scrollable: none) { + @slot; + } +} + +@custom-variant container-not-scrollable { + @container scroll-state(scrollable: none) { + @slot; + } +} diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index 1d4037b216..e3a1432d5e 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -36,6 +36,10 @@ const SemverKeys = [ 'desktop.donationPaypal.prod', 'desktop.groupMemberLabels.edit.beta', 'desktop.groupMemberLabels.edit.prod', + 'desktop.adminDelete.receive.beta', + 'desktop.adminDelete.receive.prod', + 'desktop.adminDelete.send.beta', + 'desktop.adminDelete.send.prod', 'desktop.pinnedMessages.receive.beta', 'desktop.pinnedMessages.receive.prod', 'desktop.pinnedMessages.send.beta', @@ -81,6 +85,8 @@ const ScalarKeys = [ 'global.nicknames.min', 'global.pinned_message_limit', 'global.textAttachmentLimitBytes', + 'global.normalDeleteMaxAgeInSeconds', + 'global.adminDeleteMaxAgeInSeconds', ] as const; // These keys should always match those in Net.REMOTE_CONFIG_KEYS, prefixed by diff --git a/ts/axo/AxoAlertDialog.dom.tsx b/ts/axo/AxoAlertDialog.dom.tsx index 514cbcd7eb..27bd993347 100644 --- a/ts/axo/AxoAlertDialog.dom.tsx +++ b/ts/axo/AxoAlertDialog.dom.tsx @@ -9,6 +9,7 @@ import { tw } from './tw.dom.js'; import { AxoBaseDialog } from './_internal/AxoBaseDialog.dom.js'; import { AxoScrollArea } from './AxoScrollArea.dom.js'; import type { AxoSymbol } from './AxoSymbol.dom.js'; +import { FlexWrapDetector } from './_internal/FlexWrapDetector.dom.js'; const Namespace = 'AxoAlertDialog'; @@ -138,7 +139,24 @@ export namespace AxoAlertDialog { }>; export const Footer: FC = memo(props => { - return
{props.children}
; + return ( + +
+ {props.children} +
+
+ ); }); Footer.displayName = `${Namespace}.Footer`; @@ -201,7 +219,7 @@ export namespace AxoAlertDialog { export const Cancel: FC = memo(props => { return ( - + {props.children} @@ -215,25 +233,36 @@ export namespace AxoAlertDialog { * ---------------------------------- */ - export type ActionVariant = 'primary' | 'secondary' | 'destructive'; + export type ActionVariant = + | 'primary' + | 'secondary' + | 'destructive' + | 'subtle-destructive'; export type ActionProps = Readonly<{ variant: ActionVariant; symbol?: AxoSymbol.InlineGlyphName; arrow?: boolean; onClick: (event: MouseEvent) => void; + disabled?: boolean; + focusableWhenDisabled?: boolean; children: ReactNode; }>; export const Action: FC = memo(props => { return ( - + {props.children} diff --git a/ts/axo/AxoButton.dom.tsx b/ts/axo/AxoButton.dom.tsx index 04176a4daf..e8fd038518 100644 --- a/ts/axo/AxoButton.dom.tsx +++ b/ts/axo/AxoButton.dom.tsx @@ -220,6 +220,7 @@ export namespace AxoButton { arrow?: boolean; experimentalSpinner?: { 'aria-label': string } | null; disabled?: GenericButtonProps['disabled']; + focusableWhenDisabled?: boolean; onClick?: GenericButtonProps['onClick']; children: ReactNode; // Note: Technically we forward all props for Radix, but we restrict the @@ -235,6 +236,8 @@ export namespace AxoButton { symbol, arrow, experimentalSpinner, + disabled, + focusableWhenDisabled, children, ...rest } = props; @@ -251,6 +254,8 @@ export namespace AxoButton { return (