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 18662a280d..d426ad6ed2 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', @@ -83,6 +87,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 (