diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index f06dcccada..6b3b5a4c50 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1526,18 +1526,70 @@
"messageformat": "This media is not available",
"description": "Shown in info toast for messages with old image and video attachments which are no longer available for download. Also used for accessibility label for the download attachment button."
},
+ "icu:mediaNotAvailable--short": {
+ "messageformat": "Media not available",
+ "description": "Title of info dialog shown for messages with missing old image and video attachments which are no longer available for download."
+ },
"icu:attachmentNoLongerAvailable__learnMore": {
"messageformat": "Learn more",
"description": "Link in message placeholder and info toast for messages with old attachments which are no longer available for download."
},
- "icu:fileNotAvailable": {
- "messageformat": "This file is not available",
- "description": "Shown in chat timeline for messages with old generic file attachments which are no longer available for download."
+ "icu:attachmentNotAvailable__file": {
+ "messageformat": "File not available",
+ "description": "Shown in chat timeline for messages with old file attachments which are no longer available for download."
},
- "icu:voiceMessageNotAvailable": {
- "messageformat": "This voice message is not available",
+ "icu:attachmentNotAvailable__longMessage": {
+ "messageformat": "This message is incomplete",
+ "description": "Shown in chat timeline for long messages when the complete messages are no longer available for download."
+ },
+ "icu:attachmentNotAvailable__sticker": {
+ "messageformat": "Sticker not available",
"description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download."
},
+ "icu:attachmentNotAvailable__voice": {
+ "messageformat": "Voice message not available",
+ "description": "Shown in chat timeline for messages with old audio attachments which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__title--file": {
+ "messageformat": "File not available",
+ "description": "Title for info dialog for messages with old file attachments which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__title--long-text": {
+ "messageformat": "Complete message not available",
+ "description": "Title for info dialog for long messages which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__title--media": {
+ "messageformat": "Media not available",
+ "description": "Title for info dialog for messages with old visual media which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__title--sticker": {
+ "messageformat": "Sticker not available",
+ "description": "Title for info dialog for old sticker messages which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__title--voice-message": {
+ "messageformat": "Voice message not available",
+ "description": "Title for info dialog for messages with old voice messages which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__body--file": {
+ "messageformat": "This file was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
+ "description": "Body text for info dialog for messages with old file attachments which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__body--long-text": {
+ "messageformat": "The complete text in this message was not transferred from your phone when this device was linked. Long text messages older than 45 days at the time of device linking can not be synced.",
+ "description": "Body text for info dialog for long messages which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__body--media": {
+ "messageformat": "This media was not transferred from your phone when this device was linked. Media older than 45 days at the time of device linking can not be synced.",
+ "description": "Body text for info dialog for messages with old visual media which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__body--sticker": {
+ "messageformat": "This sticker was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
+ "description": "Body text for info dialog for old sticker messages which are no longer available for download."
+ },
+ "icu:AttachmentNotAvailableModal__body--voice-message": {
+ "messageformat": "This voice message was not transferred from your phone when this device was linked. Media and files older than 45 days at the time of device linking can not be synced.",
+ "description": "Body text for info dialog for messages with old voice messages which are no longer available for download."
+ },
"icu:save": {
"messageformat": "Save",
"description": "Used on save buttons"
diff --git a/images/icons/v3/file/file-slash.svg b/images/icons/v3/file/file-slash.svg
deleted file mode 100644
index 8a19a92017..0000000000
--- a/images/icons/v3/file/file-slash.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/images/icons/v3/sticker/sticker-slash.svg b/images/icons/v3/sticker/sticker-slash.svg
new file mode 100644
index 0000000000..a420eee601
--- /dev/null
+++ b/images/icons/v3/sticker/sticker-slash.svg
@@ -0,0 +1,5 @@
+
diff --git a/images/icons/v3/play/play-slash.svg b/images/icons/v3/waveform/waveform-slash.svg
similarity index 63%
rename from images/icons/v3/play/play-slash.svg
rename to images/icons/v3/waveform/waveform-slash.svg
index c2a25b1ed1..8f5d91788b 100644
--- a/images/icons/v3/play/play-slash.svg
+++ b/images/icons/v3/waveform/waveform-slash.svg
@@ -1,7 +1,11 @@
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 5098219c2a..67fe242bb5 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -594,6 +594,7 @@ $message-padding-horizontal: 12px;
}
.module-message__attachment-too-big--content-above {
+ margin-block-start: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@@ -784,6 +785,11 @@ $message-padding-horizontal: 12px;
margin-block-start: -25px;
}
+.module-message__generic-attachment--undownloadable-no-text
+ + .module-message__metadata {
+ margin-block-start: -$message-padding-vertical - 2px;
+}
+
.module-message__sticker-container {
// To ensure that images are centered if they aren't full width of bubble
text-align: center;
@@ -825,6 +831,10 @@ $message-padding-horizontal: 12px;
}
}
+.module-message__generic-attachment--undownloadable {
+ min-width: 260px;
+}
+
.module-message__generic-attachment--with-content-below {
padding-bottom: 6px;
}
@@ -896,18 +906,21 @@ $message-padding-horizontal: 12px;
color: variables.$color-gray-90;
}
+$message-attachment-padding-horizontal: 8px;
+
.module-message__generic-attachment__text {
flex-grow: 1;
- margin-inline-start: 8px;
+ margin-inline-start: $message-attachment-padding-horizontal + 2px;
// The width of the icon plus our 8px margin plus 1px leeway
max-width: calc(100% - 36px);
}
.module-message__generic-attachment__file-name {
- @include mixins.font-body-2-bold;
+ @include mixins.font-body-1;
margin-top: 2px;
user-select: none;
+ font-weight: 500;
// Handling really long filenames - cut them off
overflow-x: hidden;
@@ -933,6 +946,11 @@ $message-padding-horizontal: 12px;
}
}
+.module-message__container--incoming
+ .module-message__generic-attachment__file-name--undownloadable {
+ color: variables.$color-black-alpha-50;
+}
+
.module-message__generic-attachment__file-size {
@include mixins.font-body-2;
@@ -959,7 +977,12 @@ $message-padding-horizontal: 12px;
}
.module-message__undownloadable-attachment__icon-container {
- margin-inline-end: 8px;
+ margin-inline-end: $message-attachment-padding-horizontal;
+}
+
+.module-message__undownloadable-attachment__icon-container--file {
+ margin-block-start: 1px;
+ margin-inline-end: 3px;
}
.module-message__undownloadable-attachment__icon {
@@ -968,35 +991,103 @@ $message-padding-horizontal: 12px;
&--audio {
@include mixins.color-svg(
- '../images/icons/v3/play/play-slash.svg',
+ '../images/icons/v3/waveform/waveform-slash.svg',
currentColor
);
}
- &--generic {
+ &--file {
@include mixins.color-svg(
- '../images/icons/v3/file/file-slash.svg',
+ '../images/icons/v3/error/error-circle.svg',
currentColor
);
}
+
+ &--sticker {
+ @include mixins.color-svg(
+ '../images/icons/v3/sticker/sticker-slash.svg',
+ currentColor
+ );
+ }
+
+ &--small {
+ width: 16px;
+ height: 16px;
+ }
}
-.module-message__undownloadable-attachment-info {
- margin-inline-end: 15px;
+.module-message__undownloadable-attachment-info--file {
+ @include mixins.font-body-2;
}
-.module-message__undownloadable-attachment-learn-more-container {
- font-weight: 500;
+.module-message__container--incoming
+ .module-message__undownloadable-attachment-info--file {
+ color: variables.$color-black-alpha-90;
+}
+
+.module-message__undownloadable-attachment {
+ min-width: 200px;
+}
+
+.module-message__undownloadable-attachment-file {
+ @include mixins.font-body-2;
+ display: flex;
+}
+
+.module-message__undownloadable-attachment-text {
+ @include mixins.button-reset;
+ & {
+ @include mixins.font-body-1;
+ @include mixins.light-theme {
+ border-block-start-color: variables.$color-black-alpha-10;
+ }
+ @include mixins.dark-theme {
+ border-block-start-color: variables.$color-white-alpha-10;
+ }
+
+ width: calc(100% + 24px);
+ margin-block-start: 9px;
+ margin-inline: -12px;
+ padding-block-start: 9px;
+ padding-inline: 12px;
+ border-block-start: 0.5px solid;
+ }
+}
+
+.module-message__container--outgoing
+ .module-message__undownloadable-attachment-text {
+ border-block-start-color: variables.$color-white-alpha-30;
+}
+
+.module-message__undownloadable-attachment-text__icon-container {
+ margin-inline-end: 8px;
+ align-self: flex-start;
+}
+
+.module-message--outgoing {
+ .module-message__undownloadable-attachment + .module-message__metadata {
+ .module-message__metadata__date--with-sticker {
+ color: inherit;
+ }
+
+ .module-message__metadata__status-icon--with-sticker {
+ background-color: variables.$color-white-alpha-80;
+ }
+ }
}
.module-message__undownloadable-attachment-learn-more {
@include mixins.button-reset;
- @include mixins.font-body-1-bold;
@include mixins.keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px variables.$color-ultramarine;
}
}
+
+ & {
+ @include mixins.font-body-1-bold;
+ font-weight: 500;
+ }
}
.module-message__link-preview {
diff --git a/stylesheets/components/AttachmentNotAvailableModal.scss b/stylesheets/components/AttachmentNotAvailableModal.scss
new file mode 100644
index 0000000000..cd899b808f
--- /dev/null
+++ b/stylesheets/components/AttachmentNotAvailableModal.scss
@@ -0,0 +1,22 @@
+// Copyright 2025 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+@use '../mixins';
+@use '../variables';
+
+.AttachmentNotAvailableModal__width-container {
+ max-width: 440px;
+}
+
+.AttachmentNotAvailableModal .AttachmentNotAvailableModal__headerTitle {
+ padding-block-end: 5px;
+}
+
+.AttachmentNotAvailableModal__body {
+ padding-block: 16px 0;
+ padding-inline: 16px;
+}
+
+.AttachmentNotAvailableModal .module-Button {
+ padding-inline: 24px;
+}
diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss
index a1bc75b0db..ead04b1923 100644
--- a/stylesheets/manifest.scss
+++ b/stylesheets/manifest.scss
@@ -25,6 +25,7 @@
@use 'components/AnnouncementsOnlyGroupBanner.scss';
@use 'components/App.scss';
@use 'components/AttachmentDetailPill.scss';
+@use 'components/AttachmentNotAvailableModal.scss';
@use 'components/AudioCapture.scss';
@use 'components/AutoSizeInput.scss';
@use 'components/Avatar.scss';
diff --git a/ts/components/AttachmentNotAvailableModal.stories.tsx b/ts/components/AttachmentNotAvailableModal.stories.tsx
new file mode 100644
index 0000000000..5854a21399
--- /dev/null
+++ b/ts/components/AttachmentNotAvailableModal.stories.tsx
@@ -0,0 +1,70 @@
+// Copyright 2025 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import React from 'react';
+import { action } from '@storybook/addon-actions';
+import { setupI18n } from '../util/setupI18n';
+import enMessages from '../../_locales/en/messages.json';
+import type { PropsType } from './AttachmentNotAvailableModal';
+import {
+ AttachmentNotAvailableModal,
+ AttachmentNotAvailableModalType,
+} from './AttachmentNotAvailableModal';
+import type { ComponentMeta } from '../storybook/types';
+
+const i18n = setupI18n('en', enMessages);
+
+export default {
+ title: 'Components/AttachmentNotAvailableModal',
+ component: AttachmentNotAvailableModal,
+ args: {
+ modalType: AttachmentNotAvailableModalType.VisualMedia,
+ i18n,
+ onClose: action('onClose'),
+ },
+} satisfies ComponentMeta;
+
+export function File(args: PropsType): JSX.Element {
+ return (
+
+ );
+}
+
+export function LongText(args: PropsType): JSX.Element {
+ return (
+
+ );
+}
+
+export function Sticker(args: PropsType): JSX.Element {
+ return (
+
+ );
+}
+
+export function VisualMedia(args: PropsType): JSX.Element {
+ return (
+
+ );
+}
+
+export function VoiceMessage(args: PropsType): JSX.Element {
+ return (
+
+ );
+}
diff --git a/ts/components/AttachmentNotAvailableModal.tsx b/ts/components/AttachmentNotAvailableModal.tsx
new file mode 100644
index 0000000000..02694a19ea
--- /dev/null
+++ b/ts/components/AttachmentNotAvailableModal.tsx
@@ -0,0 +1,92 @@
+// Copyright 2025 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import * as React from 'react';
+
+import type { LocalizerType } from '../types/Util';
+import { Modal } from './Modal';
+import { Button, ButtonVariant } from './Button';
+
+export type PropsType = {
+ i18n: LocalizerType;
+ modalType: AttachmentNotAvailableModalType;
+ onClose: () => void;
+};
+
+export enum AttachmentNotAvailableModalType {
+ File = 'File',
+ LongText = 'LongText',
+ Sticker = 'Sticker',
+ VisualMedia = 'VisualMedia',
+ VoiceMessage = 'VoiceMessage',
+}
+
+function focusRef(el: HTMLElement | null) {
+ if (el) {
+ el.focus();
+ }
+}
+
+function getTitle(
+ i18n: LocalizerType,
+ modalType: AttachmentNotAvailableModalType
+): string {
+ switch (modalType) {
+ case AttachmentNotAvailableModalType.LongText:
+ return i18n('icu:AttachmentNotAvailableModal__title--long-text');
+ case AttachmentNotAvailableModalType.Sticker:
+ return i18n('icu:AttachmentNotAvailableModal__title--sticker');
+ case AttachmentNotAvailableModalType.VisualMedia:
+ return i18n('icu:AttachmentNotAvailableModal__title--media');
+ case AttachmentNotAvailableModalType.VoiceMessage:
+ return i18n('icu:AttachmentNotAvailableModal__title--voice-message');
+ case AttachmentNotAvailableModalType.File:
+ default:
+ return i18n('icu:AttachmentNotAvailableModal__title--file');
+ }
+}
+
+function getBody(
+ i18n: LocalizerType,
+ modalType: AttachmentNotAvailableModalType
+): string {
+ switch (modalType) {
+ case AttachmentNotAvailableModalType.LongText:
+ return i18n('icu:AttachmentNotAvailableModal__body--long-text');
+ case AttachmentNotAvailableModalType.Sticker:
+ return i18n('icu:AttachmentNotAvailableModal__body--sticker');
+ case AttachmentNotAvailableModalType.VisualMedia:
+ return i18n('icu:AttachmentNotAvailableModal__body--media');
+ case AttachmentNotAvailableModalType.VoiceMessage:
+ return i18n('icu:AttachmentNotAvailableModal__body--voice-message');
+ case AttachmentNotAvailableModalType.File:
+ default:
+ return i18n('icu:AttachmentNotAvailableModal__body--file');
+ }
+}
+
+export function AttachmentNotAvailableModal(props: PropsType): JSX.Element {
+ const { i18n, modalType, onClose } = props;
+
+ const footer = (
+
+ );
+
+ return (
+
+