From 1cc26d5cc79326c3d50dd5fcd3fefe03b37c8d6f Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:15:32 -0800 Subject: [PATCH] Show info for permanently undownloadable visual attachments --- _locales/en/messages.json | 8 ++ images/icons/v3/photo/photo-slash-compact.svg | 5 + stylesheets/_modules.scss | 15 +++ ts/components/EditHistoryMessagesModal.tsx | 1 + ts/components/StoryViewsNRepliesModal.tsx | 1 + ts/components/ToastManager.stories.tsx | 2 + ts/components/ToastManager.tsx | 16 +++ ts/components/conversation/GIF.tsx | 29 +++- ts/components/conversation/Image.tsx | 31 ++++- ts/components/conversation/ImageGrid.tsx | 31 ++++- ts/components/conversation/Message.tsx | 14 +- ts/components/conversation/MessageDetail.tsx | 3 + ts/components/conversation/Quote.stories.tsx | 1 + .../conversation/Timeline.stories.tsx | 1 + .../conversation/TimelineItem.stories.tsx | 1 + .../conversation/TimelineMessage.stories.tsx | 127 ++++++++++++++++++ ts/hooks/useUndownloadableMediaHandler.tsx | 19 +++ ts/state/ducks/conversations.ts | 9 ++ ts/state/selectors/message.ts | 7 +- ts/state/smart/MessageDetail.tsx | 2 + ts/state/smart/TimelineItem.tsx | 2 + ts/types/Toast.tsx | 2 + ts/types/support.ts | 2 + 23 files changed, 314 insertions(+), 15 deletions(-) create mode 100644 images/icons/v3/photo/photo-slash-compact.svg create mode 100644 ts/hooks/useUndownloadableMediaHandler.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c0559d9f89..f15b65a741 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1526,6 +1526,14 @@ "messageformat": "{count, plural, one {# item} other {# items}}", "description": "Describes a button shown on a grid of attachments to start of them downloading" }, + "icu:mediaNoLongerAvailable": { + "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: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:save": { "messageformat": "Save", "description": "Used on save buttons" diff --git a/images/icons/v3/photo/photo-slash-compact.svg b/images/icons/v3/photo/photo-slash-compact.svg new file mode 100644 index 0000000000..f4ebe5f528 --- /dev/null +++ b/images/icons/v3/photo/photo-slash-compact.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e828112b34..d40e9cce0c 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2864,6 +2864,7 @@ button.module-image__border-overlay:focus { bottom: 0; z-index: variables.$z-index-base; inset-inline: 0; + pointer-events: none; } .module-image__overlay-circle { @@ -2877,6 +2878,10 @@ button.module-image__border-overlay:focus { } } +.module-image__overlay-circle--undownloadable { + background-color: variables.$color-black-alpha-40; +} + .module-image__play-icon { @include mixins.position-absolute-center; @@ -2908,6 +2913,16 @@ button.module-image__border-overlay:focus { variables.$color-white ); } +.module-image__undownloadable-icon { + @include mixins.position-absolute-center; + + height: 24px; + width: 24px; + @include mixins.color-svg( + '../images/icons/v3/photo/photo-slash-compact.svg', + variables.$color-white + ); +} .module-image__text-container { position: absolute; diff --git a/ts/components/EditHistoryMessagesModal.tsx b/ts/components/EditHistoryMessagesModal.tsx index c49f4dada1..660868c695 100644 --- a/ts/components/EditHistoryMessagesModal.tsx +++ b/ts/components/EditHistoryMessagesModal.tsx @@ -65,6 +65,7 @@ const MESSAGE_DEFAULT_PROPS = { showExpiredIncomingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled, + showMediaNoLongerAvailableToast: shouldNeverBeCalled, startConversation: shouldNeverBeCalled, textDirection: TextDirection.Default, viewStory: shouldNeverBeCalled, diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index 6781b6fc0b..58eb764f9a 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -73,6 +73,7 @@ const MESSAGE_DEFAULT_PROPS = { showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showLightbox: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled, + showMediaNoLongerAvailableToast: shouldNeverBeCalled, startConversation: shouldNeverBeCalled, theme: ThemeType.dark, viewStory: shouldNeverBeCalled, diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 73df1860d8..d4fe3b2f4d 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -129,6 +129,8 @@ function getToast(toastType: ToastType): AnyToast { return { toastType: ToastType.LoadingFullLogs }; case ToastType.MaxAttachments: return { toastType: ToastType.MaxAttachments }; + case ToastType.MediaNoLongerAvailable: + return { toastType: ToastType.MediaNoLongerAvailable }; case ToastType.MessageBodyTooLong: return { toastType: ToastType.MessageBodyTooLong }; case ToastType.MessageLoop: diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index f6b3c727e2..1e7b6b7438 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -16,6 +16,8 @@ import type { AnyToast } from '../types/Toast'; import { ToastType } from '../types/Toast'; import type { AnyActionableMegaphone } from '../types/Megaphone'; import { MegaphoneType } from '../types/Megaphone'; +import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; +import { LINKED_DEVICES_URL } from '../types/support'; export type PropsType = { hideToast: () => unknown; @@ -407,6 +409,20 @@ export function renderToast({ return {i18n('icu:maximumAttachments')}; } + if (toastType === ToastType.MediaNoLongerAvailable) { + return ( + openLinkInWebBrowser(LINKED_DEVICES_URL), + }} + > + {i18n('icu:mediaNoLongerAvailable')} + + ); + } + if (toastType === ToastType.MessageBodyTooLong) { return {i18n('icu:messageBodyTooLong')}; } diff --git a/ts/components/conversation/GIF.tsx b/ts/components/conversation/GIF.tsx index 9f39e05469..0bcba74015 100644 --- a/ts/components/conversation/GIF.tsx +++ b/ts/components/conversation/GIF.tsx @@ -12,12 +12,14 @@ import { hasNotResolved, getImageDimensions, defaultBlurHash, + isDownloadable, } from '../../types/Attachment'; import * as Errors from '../../types/errors'; import * as log from '../../logging/log'; import { useReducedMotion } from '../../hooks/useReducedMotion'; import { AttachmentDetailPill } from './AttachmentDetailPill'; import { getSpinner } from './Image'; +import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler'; const MAX_GIF_REPEAT = 4; const MAX_GIF_TIME = 8; @@ -33,6 +35,7 @@ export type Props = { readonly theme?: ThemeType; onError(): void; + showMediaNoLongerAvailableToast?: () => void; showVisualAttachment(): void; startDownload(): void; cancelDownload(): void; @@ -51,6 +54,7 @@ export function GIF(props: Props): JSX.Element { theme, onError, + showMediaNoLongerAvailableToast, showVisualAttachment, startDownload, cancelDownload, @@ -123,6 +127,10 @@ export function GIF(props: Props): JSX.Element { setIsPlaying(isFocused && !isTapToPlayPaused); }, [isFocused, playTime, currentTime, repeatCount, tapToPlay]); + const undownloadableClick = useUndownloadableMediaHandler( + showMediaNoLongerAvailableToast + ); + const onTimeUpdate = async (event: MediaEvent): Promise => { const { currentTime: reportedTime } = event.currentTarget; if (!Number.isNaN(reportedTime)) { @@ -175,9 +183,10 @@ export function GIF(props: Props): JSX.Element { const isPending = Boolean(attachment.pending); const isNotResolved = hasNotResolved(attachment) && !isPending; + const isMediaDownloadable = isDownloadable(attachment); let gif: JSX.Element | undefined; - if (isNotResolved || isPending) { + if (isNotResolved || isPending || !isMediaDownloadable) { gif = ( ); + } else if (!isMediaDownloadable) { + overlay = ( + + ); } - const detailPill = ( + const detailPill = isDownloadable(attachment) ? ( - ); + ) : null; return (
diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index f595dd1ded..077e0fef45 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -14,10 +14,12 @@ import type { } from '../../types/Attachment'; import { defaultBlurHash, + isDownloadable, isIncremental, isReadyToView, } from '../../types/Attachment'; import { ProgressCircle } from '../ProgressCircle'; +import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler'; export enum CurveType { None = 0, @@ -55,6 +57,7 @@ export type Props = { i18n: LocalizerType; theme?: ThemeType; + showMediaNoLongerAvailableToast?: () => void; showVisualAttachment?: (attachment: AttachmentType) => void; cancelDownload?: () => void; startDownload?: () => void; @@ -78,6 +81,7 @@ export function Image({ i18n, noBackground, noBorder, + showMediaNoLongerAvailableToast, showVisualAttachment, startDownload, cancelDownload, @@ -164,6 +168,9 @@ export function Image({ }, [startDownload] ); + const undownloadableClick = useUndownloadableMediaHandler( + showMediaNoLongerAvailableToast + ); const imageOrBlurHash = url ? ( {imageOrBlurHash} - {startDownloadButton} + {isMediaDownloadable ? ( + startDownloadButton + ) : ( + + )} {spinner} {attachment.caption ? ( @@ -245,7 +266,9 @@ export function Image({ }} /> ) : null} - {(attachment.path || isIncremental(attachment)) && playIconOverlay ? ( + {(attachment.path || isIncremental(attachment)) && + isMediaDownloadable && + playIconOverlay ? (
@@ -267,7 +290,9 @@ export function Image({ style={curveStyles} /> ) : null} - {showVisualAttachment && isReadyToView(attachment) ? ( + {showVisualAttachment && + isReadyToView(attachment) && + isMediaDownloadable ? (
); @@ -1043,11 +1047,13 @@ export class Message extends React.PureComponent { cancelDownload={() => { cancelAttachmentDownload({ messageId: id }); }} + showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} />
); } } + if (isAudio(attachments)) { const played = isPlayed(direction, status, readStatus); @@ -1165,6 +1171,7 @@ export class Message extends React.PureComponent { id, kickOffAttachmentDownload, cancelAttachmentDownload, + showMediaNoLongerAvailableToast, previews, quote, shouldCollapseAbove, @@ -1237,6 +1244,7 @@ export class Message extends React.PureComponent { cancelDownload={() => { cancelAttachmentDownload({ messageId: id }); }} + showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} /> ) : null}
@@ -1264,6 +1272,9 @@ export class Message extends React.PureComponent { blurHash={first.image.blurHash} onError={this.handleImageError} i18n={i18n} + showMediaNoLongerAvailableToast={ + showMediaNoLongerAvailableToast + } showVisualAttachment={() => { openLinkInWebBrowser(first.url); }} @@ -2599,7 +2610,8 @@ export class Message extends React.PureComponent { attachments && attachments.length > 0 && !isAttachmentPending && - !isDownloaded(attachments[0]) + !isDownloaded(attachments[0]) && + isDownloadable(attachments[0]) ) { event.preventDefault(); event.stopPropagation(); diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 34a7b378c7..0a826fb257 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -105,6 +105,7 @@ export type PropsReduxActions = Pick< | 'showExpiredOutgoingTapToViewToast' | 'showLightbox' | 'showLightboxForViewOnceMedia' + | 'showMediaNoLongerAvailableToast' | 'showSpoiler' | 'startConversation' | 'viewStory' @@ -152,6 +153,7 @@ export function MessageDetail({ showExpiredOutgoingTapToViewToast, showLightbox, showLightboxForViewOnceMedia, + showMediaNoLongerAvailableToast, showSpoiler, startConversation, theme, @@ -375,6 +377,7 @@ export function MessageDetail({ showExpiredOutgoingTapToViewToast } showLightbox={showLightbox} + showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} startConversation={startConversation} theme={theme} viewStory={viewStory} diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index ae9bf2d0c2..e45e5447cb 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -144,6 +144,7 @@ const defaultMessageProps: TimelineMessagesProps = { showExpiredOutgoingTapToViewToast: action( 'showExpiredOutgoingTapToViewToast' ), + showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'), toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'), showLightbox: action('default--showLightbox'), diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index f8a5194978..e51a3e10ca 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -316,6 +316,7 @@ const actions = () => ({ showExpiredOutgoingTapToViewToast: action( 'showExpiredOutgoingTapToViewToast' ), + showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'), diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 4f0fa532a3..c0d9a79c8c 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -107,6 +107,7 @@ const getDefaultProps = () => ({ showExpiredOutgoingTapToViewToast: action( 'showExpiredIncomingTapToViewToast' ), + showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), scrollToQuotedMessage: action('scrollToQuotedMessage'), showSpoiler: action('showSpoiler'), startConversation: action('startConversation'), diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 0685ba757b..203c9a7faf 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -351,6 +351,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ showExpiredOutgoingTapToViewToast: action( 'showExpiredOutgoingTapToViewToast' ), + showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'), toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'), showLightbox: action('showLightbox'), @@ -2198,3 +2199,129 @@ export function MultiSelect(): JSX.Element { MultiSelect.args = { name: 'Multi Select', }; + +export function PermanentlyUndownloadableAttachments(): JSX.Element { + const imageProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: IMAGE_JPEG, + fileName: 'bird.jpg', + blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', + width: 296, + height: 394, + path: undefined, + key: undefined, + id: undefined, + }), + ], + status: 'sent', + }); + const multipleImagesProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: IMAGE_JPEG, + fileName: 'bird.jpg', + blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', + width: 296, + height: 394, + path: undefined, + key: undefined, + id: undefined, + }), + fakeAttachment({ + contentType: IMAGE_JPEG, + fileName: 'bird.jpg', + blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', + width: 296, + height: 394, + path: undefined, + key: undefined, + id: undefined, + }), + ], + status: 'sent', + }); + const gifProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: VIDEO_MP4, + flags: SignalService.AttachmentPointer.Flags.GIF, + fileName: 'bird.gif', + blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', + width: 296, + height: 394, + path: undefined, + key: undefined, + id: undefined, + }), + ], + status: 'sent', + text: 'cool gif', + }); + const videoProps = createProps({ + attachments: [ + fakeAttachment({ + contentType: VIDEO_MP4, + fileName: 'bird.mp4', + width: 720, + height: 480, + path: undefined, + key: undefined, + id: undefined, + }), + ], + status: 'sent', + }); + + const outgoingAuthor = { + ...imageProps.author, + id: getDefaultConversation().id, + }; + + return ( + <> + + + + + + + + + + ); +} + +export const AttachmentWithError = Template.bind({}); +AttachmentWithError.args = { + attachments: [ + fakeAttachment({ + contentType: IMAGE_PNG, + fileName: 'test.png', + blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', + width: 296, + height: 394, + path: undefined, + error: true, + }), + ], + status: 'sent', +}; diff --git a/ts/hooks/useUndownloadableMediaHandler.tsx b/ts/hooks/useUndownloadableMediaHandler.tsx new file mode 100644 index 0000000000..96834a9c75 --- /dev/null +++ b/ts/hooks/useUndownloadableMediaHandler.tsx @@ -0,0 +1,19 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { useCallback } from 'react'; + +export function useUndownloadableMediaHandler( + showMediaNoLongerAvailableToast: (() => void) | undefined +): (event: React.MouseEvent) => void { + return useCallback( + (event: React.MouseEvent) => { + if (showMediaNoLongerAvailableToast) { + event.preventDefault(); + event.stopPropagation(); + showMediaNoLongerAvailableToast(); + } + }, + [showMediaNoLongerAvailableToast] + ); +} diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b5414de0af..580841b425 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -1209,6 +1209,7 @@ export const actions = { showFindByUsername, showFindByPhoneNumber, showInbox, + showMediaNoLongerAvailableToast, startComposing, startConversation, startSettingGroupMetadata, @@ -4563,6 +4564,14 @@ function showInbox(): ShowInboxActionType { payload: null, }; } +function showMediaNoLongerAvailableToast(): ShowToastActionType { + return { + type: SHOW_TOAST, + payload: { + toastType: ToastType.MediaNoLongerAvailable, + }, + }; +} type ShowConversationArgsType = ReadonlyDeep<{ conversationId?: string; diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 61526c9fcf..9570ea7e57 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -66,11 +66,7 @@ import type { AttachmentForUIType, AttachmentType, } from '../../types/Attachment'; -import { - isVoiceMessage, - canBeDownloaded, - defaultBlurHash, -} from '../../types/Attachment'; +import { isVoiceMessage, defaultBlurHash } from '../../types/Attachment'; import { type DefaultConversationColorType } from '../../types/Colors'; import { ReadStatus } from '../../messages/MessageReadStatus'; @@ -325,7 +321,6 @@ export const getAttachmentsForMessage = ({ } return ( attachments - .filter(attachment => !attachment.error || canBeDownloaded(attachment)) // Long message attachments are removed from message.attachments quickly, // but in case they are still around, let's make sure not to show them .filter(attachment => attachment.contentType !== LONG_MESSAGE) diff --git a/ts/state/smart/MessageDetail.tsx b/ts/state/smart/MessageDetail.tsx index 7a1aa333d8..4a2e21fe64 100644 --- a/ts/state/smart/MessageDetail.tsx +++ b/ts/state/smart/MessageDetail.tsx @@ -55,6 +55,7 @@ export const SmartMessageDetail = memo( showConversation, showExpiredIncomingTapToViewToast, showExpiredOutgoingTapToViewToast, + showMediaNoLongerAvailableToast, showSpoiler, startConversation, } = useConversationsActions(); @@ -115,6 +116,7 @@ export const SmartMessageDetail = memo( showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast} showLightbox={showLightbox} showLightboxForViewOnceMedia={showLightboxForViewOnceMedia} + showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} showSpoiler={showSpoiler} startConversation={startConversation} theme={theme} diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index a10a062c5c..ed5c61674a 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -133,6 +133,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( showConversation, showExpiredIncomingTapToViewToast, showExpiredOutgoingTapToViewToast, + showMediaNoLongerAvailableToast, showSpoiler, startConversation, targetMessage, @@ -236,6 +237,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast} showLightbox={showLightbox} showLightboxForViewOnceMedia={showLightboxForViewOnceMedia} + showMediaNoLongerAvailableToast={showMediaNoLongerAvailableToast} showSpoiler={showSpoiler} startConversation={startConversation} toggleDeleteMessagesModal={toggleDeleteMessagesModal} diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index 326edbe065..6ed5841ad1 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -44,6 +44,7 @@ export enum ToastType { LinkCopied = 'LinkCopied', LoadingFullLogs = 'LoadingFullLogs', MaxAttachments = 'MaxAttachments', + MediaNoLongerAvailable = 'MediaNoLongerAvailable', MessageBodyTooLong = 'MessageBodyTooLong', MessageLoop = 'MessageLoop', OriginalMessageNotFound = 'OriginalMessageNotFound', @@ -136,6 +137,7 @@ export type AnyToast = | { toastType: ToastType.LinkCopied } | { toastType: ToastType.LoadingFullLogs } | { toastType: ToastType.MaxAttachments } + | { toastType: ToastType.MediaNoLongerAvailable } | { toastType: ToastType.MessageBodyTooLong } | { toastType: ToastType.MessageLoop } | { toastType: ToastType.OriginalMessageNotFound } diff --git a/ts/types/support.ts b/ts/types/support.ts index 4fd97f09c5..2fc93dd641 100644 --- a/ts/types/support.ts +++ b/ts/types/support.ts @@ -5,6 +5,8 @@ export const PRODUCTION_DOWNLOAD_URL = 'https://signal.org/download/'; export const BETA_DOWNLOAD_URL = 'https://support.signal.org/beta'; export const UNSUPPORTED_OS_URL = 'https://support.signal.org/hc/articles/5109141421850'; +export const LINKED_DEVICES_URL = + 'https://support.signal.org/hc/en-us/articles/360007320551-Linked-Devices'; export const LINK_SIGNAL_DESKTOP = 'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device'; export const SAFETY_NUMBER_URL =