From bd88f7558a42c23d1d2ade7bab42ab37e6e972c5 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:58:22 -0600 Subject: [PATCH] Show info for permanently undownloadable file and audio attachments Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- _locales/en/messages.json | 12 +++ images/icons/v3/file/file-slash.svg | 5 ++ images/icons/v3/play/play-slash.svg | 7 ++ stylesheets/_modules.scss | 81 +++++++++++++++++++ stylesheets/_variables.scss | 3 + ts/components/ToastManager.tsx | 2 +- ts/components/conversation/GIF.tsx | 2 +- ts/components/conversation/Image.tsx | 2 +- ts/components/conversation/Message.tsx | 53 +++++++++++- .../conversation/TimelineMessage.stories.tsx | 69 +++++++++++++++- ts/types/Attachment.ts | 14 ++++ 11 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 images/icons/v3/file/file-slash.svg create mode 100644 images/icons/v3/play/play-slash.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 940bda81f13..ee2a2b0f163 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1522,10 +1522,22 @@ "messageformat": "This media is no longer 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": { + "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: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:voiceMessageNotAvailable": { + "messageformat": "This voice message is not available", + "description": "Shown in chat timeline for messages with old audio attachments 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 new file mode 100644 index 00000000000..8a19a920174 --- /dev/null +++ b/images/icons/v3/file/file-slash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/images/icons/v3/play/play-slash.svg b/images/icons/v3/play/play-slash.svg new file mode 100644 index 00000000000..c2a25b1ed18 --- /dev/null +++ b/images/icons/v3/play/play-slash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index d40e9cce0c1..a2b0141b319 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -744,6 +744,46 @@ $message-padding-horizontal: 12px; } } +.module-message__undownloadable-attachment { + @include mixins.font-body-1; + + display: flex; + + padding-block: 9px 8px; + padding-inline: 12px; + + margin-inline: -$message-padding-horizontal; + margin-block-start: -$message-padding-vertical; + margin-block-end: -$message-padding-vertical; + + &--with-content-below { + margin-block-end: 7px; + border-bottom: 0.5px solid variables.$color-white-alpha-10; + + @include mixins.light-theme { + border-color: variables.$color-black-alpha-10; + } + + @include mixins.dark-theme { + border-color: variables.$color-white-alpha-10; + } + } + + &--with-content-above { + margin-block-start: 4px; + } +} + +.module-message__container--outgoing + .module-message__undownloadable-attachment--with-content-below { + border-color: variables.$color-white-alpha-30; +} + +.module-message__undownloadable-attachment--no-text + + .module-message__metadata { + margin-block-start: -25px; +} + .module-message__sticker-container { // To ensure that images are centered if they aren't full width of bubble text-align: center; @@ -918,6 +958,47 @@ $message-padding-horizontal: 12px; } } +.module-message__undownloadable-attachment__icon-container { + margin-inline-end: 8px; +} + +.module-message__undownloadable-attachment__icon { + height: 20px; + width: 20px; + + &--audio { + @include mixins.color-svg( + '../images/icons/v3/play/play-slash.svg', + currentColor + ); + } + + &--generic { + @include mixins.color-svg( + '../images/icons/v3/file/file-slash.svg', + currentColor + ); + } +} + +.module-message__undownloadable-attachment-info { + margin-inline-end: 15px; +} + +.module-message__undownloadable-attachment-learn-more-container { + font-weight: 500; +} + +.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; + } + } +} + .module-message__link-preview { cursor: pointer; diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index 34435682f3e..942c2c40e08 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -46,9 +46,11 @@ $color-black: #000000; $color-white-alpha-06: rgba($color-white, 0.06); $color-white-alpha-08: rgba($color-white, 0.08); +$color-white-alpha-10: rgba($color-white, 0.1); $color-white-alpha-12: rgba($color-white, 0.12); $color-white-alpha-16: rgba($color-white, 0.16); $color-white-alpha-20: rgba($color-white, 0.2); +$color-white-alpha-30: rgba($color-white, 0.3); $color-white-alpha-40: rgba($color-white, 0.4); $color-white-alpha-60: rgba($color-white, 0.6); $color-white-alpha-70: rgba($color-white, 0.7); @@ -60,6 +62,7 @@ $color-black-alpha-06: rgba($color-black, 0.06); $color-black-alpha-08: rgba($color-black, 0.08); // Equivalent to gray-05 on a white background $color-black-alpha-085: rgba($color-black, 0.085); +$color-black-alpha-10: rgba($color-black, 0.1); $color-black-alpha-12: rgba($color-black, 0.12); $color-black-alpha-16: rgba($color-black, 0.16); $color-black-alpha-20: rgba($color-black, 0.2); diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index 1e7b6b7438a..9166c227d84 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -418,7 +418,7 @@ export function renderToast({ onClick: () => openLinkInWebBrowser(LINKED_DEVICES_URL), }} > - {i18n('icu:mediaNoLongerAvailable')} + {i18n('icu:mediaNotAvailable')} ); } diff --git a/ts/components/conversation/GIF.tsx b/ts/components/conversation/GIF.tsx index 0bcba74015d..2f44b76541d 100644 --- a/ts/components/conversation/GIF.tsx +++ b/ts/components/conversation/GIF.tsx @@ -276,7 +276,7 @@ export function GIF(props: Props): JSX.Element { diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index 6ec8e88126b..96d155e4dd9 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -212,7 +212,7 @@ export function Image({ diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 15fad178bef..ed1ac704e0a 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -71,6 +71,7 @@ import { isGIF, isPlayed, isPermanentlyUndownloadable, + canRenderAudio, } from '../../types/Attachment'; import type { EmbeddedContactType } from '../../types/EmbeddedContact'; @@ -106,6 +107,7 @@ import { getColorForCallLink } from '../../util/getColorForCallLink'; import { getKeyFromCallLink } from '../../util/callLinks'; import { InAnotherCallTooltip } from './InAnotherCallTooltip'; import { formatFileSize } from '../../util/formatFileSize'; +import { LINKED_DEVICES_URL } from '../../types/support'; const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 16; const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18; @@ -630,7 +632,7 @@ export class Message extends React.PureComponent { } if (!text && !deletedForEveryone && !attachmentDroppedDueToSize) { - return isAudio(attachments) + return canRenderAudio(attachments) ? MetadataPlacement.RenderedByMessageAudioComponent : MetadataPlacement.Bottom; } @@ -1053,8 +1055,55 @@ export class Message extends React.PureComponent { ); } } + const isAttachmentAudio = isAudio(attachments); - if (isAudio(attachments)) { + // Undownloadable audio and generic files + if (isPermanentlyUndownloadable(firstAttachment)) { + const containerClassName = classNames( + 'module-message__undownloadable-attachment', + withContentAbove + ? 'module-message__undownloadable-attachment--with-content-above' + : null, + withContentBelow + ? 'module-message__undownloadable-attachment--with-content-below' + : null, + text ? null : 'module-message__undownloadable-attachment--no-text' + ); + const attachmentType = isAttachmentAudio ? 'audio' : 'generic'; + const iconClassName = classNames( + 'module-message__undownloadable-attachment__icon', + `module-message__undownloadable-attachment__icon--${attachmentType}` + ); + return ( + + + + + + + {isAttachmentAudio + ? i18n('icu:voiceMessageNotAvailable') + : i18n('icu:fileNotAvailable')} + + + { + e.stopPropagation(); + e.preventDefault(); + openLinkInWebBrowser(LINKED_DEVICES_URL); + }} + type="button" + > + {i18n('icu:attachmentNoLongerAvailable__learnMore')} + + + + + ); + } + + if (isAttachmentAudio) { const played = isPlayed(direction, status, readStatus); return renderAudioAttachment({ diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 203c9a7faf6..48e2ce39795 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -2212,6 +2212,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { path: undefined, key: undefined, id: undefined, + error: true, }), ], status: 'sent', @@ -2227,6 +2228,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { path: undefined, key: undefined, id: undefined, + error: true, }), fakeAttachment({ contentType: IMAGE_JPEG, @@ -2237,6 +2239,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { path: undefined, key: undefined, id: undefined, + error: true, }), ], status: 'sent', @@ -2253,6 +2256,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { path: undefined, key: undefined, id: undefined, + error: true, }), ], status: 'sent', @@ -2268,10 +2272,49 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { path: undefined, key: undefined, id: undefined, + error: true, }), ], status: 'sent', }); + const audioProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: AUDIO_MP3, + fileName: 'bird.mp3', + width: undefined, + height: undefined, + path: undefined, + key: undefined, + id: undefined, + error: true, + }), + ], + status: 'sent', + }); + const audioWithCaptionProps = { + ...audioProps, + text: "Here's that file", + }; + const textFileProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: stringToMIMEType('text/plain'), + fileName: 'why-i-love-birds.txt', + width: undefined, + height: undefined, + path: undefined, + key: undefined, + id: undefined, + error: true, + }), + ], + status: 'sent', + }); + const textFileWithCaptionProps = { + ...textFileProps, + text: "Here's that file", + }; const outgoingAuthor = { ...imageProps.author, @@ -2283,7 +2326,11 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { - + + + + + + + + + > diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 052275df073..7b0e084d47c 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -655,6 +655,20 @@ export function isPlayed( return readStatus === ReadStatus.Viewed; } +export function canRenderAudio( + attachments?: ReadonlyArray +): boolean { + const firstAttachment = attachments && attachments[0]; + if (!firstAttachment) { + return false; + } + + return ( + isAudio(attachments) && + (isDownloaded(firstAttachment) || isDownloadable(firstAttachment)) + ); +} + export function canDisplayImage( attachments?: ReadonlyArray ): boolean {