diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index f428e9e4aa..810d3c37ea 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -52,7 +52,7 @@ import type { ShowConversationType, } from '../state/ducks/conversations'; import type { EmojiPickDataType } from './emoji/EmojiPicker'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import { isSameLinkPreview } from '../types/message/LinkPreviews'; import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileSharingActions'; @@ -135,7 +135,7 @@ export type OwnProps = Readonly<{ isSmsOnlyOrUnregistered: boolean | null; left: boolean | null; linkPreviewLoading: boolean; - linkPreviewResult: LinkPreviewType | null; + linkPreviewResult: LinkPreviewForUIType | null; onClearAttachments(conversationId: string): unknown; onCloseLinkPreview(conversationId: string): unknown; platform: string; diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index eff992e640..d860b61efc 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -61,7 +61,7 @@ import { isNotNil } from '../util/isNotNil'; import * as log from '../logging/log'; import * as Errors from '../types/errors'; import { useEmojiSearch } from '../hooks/useEmojiSearch'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import type { DraftEditMessageType } from '../model-types.d'; import { usePrevious } from '../hooks/usePrevious'; @@ -147,7 +147,7 @@ export type Props = Readonly<{ quotedMessageId: string | null; shouldHidePopovers: boolean | null; linkPreviewLoading?: boolean; - linkPreviewResult: LinkPreviewType | null; + linkPreviewResult: LinkPreviewForUIType | null; onCloseLinkPreview?(conversationId: string): unknown; }>; diff --git a/ts/components/ForwardMessagesModal.stories.tsx b/ts/components/ForwardMessagesModal.stories.tsx index ccfc8e04f0..41f92431ff 100644 --- a/ts/components/ForwardMessagesModal.stories.tsx +++ b/ts/components/ForwardMessagesModal.stories.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; -import type { AttachmentType } from '../types/Attachment'; +import type { AttachmentForUIType } from '../types/Attachment'; import type { PropsType } from './ForwardMessagesModal'; import { ForwardMessagesModal, @@ -17,8 +17,8 @@ import { CompositionTextArea } from './CompositionTextArea'; import type { MessageForwardDraft } from '../types/ForwardDraft'; const createAttachment = ( - props: Partial = {} -): AttachmentType => ({ + props: Partial = {} +): AttachmentForUIType => ({ pending: false, path: 'fileName.jpg', contentType: stringToMIMEType(props.contentType ?? ''), @@ -26,6 +26,7 @@ const createAttachment = ( screenshotPath: props.pending === false ? props.screenshotPath : undefined, url: props.pending === false ? (props.url ?? '') : '', size: 3433, + isPermanentlyUndownloadable: false, }); export default { diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx index 8d7e0b752c..971047187a 100644 --- a/ts/components/ForwardMessagesModal.tsx +++ b/ts/components/ForwardMessagesModal.tsx @@ -11,7 +11,7 @@ import React, { Fragment, } from 'react'; import { AttachmentList } from './conversation/AttachmentList'; -import type { AttachmentType } from '../types/Attachment'; +import type { AttachmentForUIType } from '../types/Attachment'; import { Button } from './Button'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; @@ -28,7 +28,7 @@ import { shouldNeverBeCalled, asyncShouldNeverBeCalled, } from '../util/shouldNeverBeCalled'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import { LinkPreviewSourceType } from '../types/LinkPreview'; import { ToastType } from '../types/Toast'; import type { ShowToastAction } from '../state/ducks/toast'; @@ -63,7 +63,7 @@ export type DataPropsType = { linkPreviewForSource: ( source: LinkPreviewSourceType - ) => LinkPreviewType | void; + ) => LinkPreviewForUIType | void; onClose: () => void; onChange: ( updatedDrafts: ReadonlyArray, @@ -435,7 +435,7 @@ export function ForwardMessagesModal({ type ForwardMessageEditorProps = Readonly<{ draft: MessageForwardDraft; - linkPreview: LinkPreviewType | null | void; + linkPreview: LinkPreviewForUIType | null | void; removeLinkPreview(): void; RenderCompositionTextArea: ComponentType; onChange: ( @@ -443,7 +443,9 @@ type ForwardMessageEditorProps = Readonly<{ bodyRanges: HydratedBodyRangesType, caretLocation?: number ) => unknown; - onChangeAttachments: (attachments: ReadonlyArray) => unknown; + onChangeAttachments: ( + attachments: ReadonlyArray + ) => unknown; onSubmit: () => unknown; theme: ThemeType; i18n: LocalizerType; @@ -482,7 +484,7 @@ function ForwardMessageEditor({ { + onCloseAttachment={attachment => { const newAttachments = attachments.filter( currentAttachment => currentAttachment !== attachment ); diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index 0486dec6bd..49c91a59a6 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -10,7 +10,7 @@ import type { InMemoryAttachmentDraftType, } from '../types/Attachment'; import type { LinkPreviewSourceType } from '../types/LinkPreview'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import type { LocalizerType, ThemeType } from '../types/Util'; import type { Props as StickerButtonProps } from './stickers/StickerButton'; import type { PropsType as SendStoryModalPropsType } from './SendStoryModal'; @@ -51,7 +51,7 @@ export type PropsType = { file?: File; i18n: LocalizerType; isSending: boolean; - linkPreview?: LinkPreviewType; + linkPreview?: LinkPreviewForUIType; onClose: () => unknown; onSend: ( listIds: Array, diff --git a/ts/components/StoryLinkPreview.tsx b/ts/components/StoryLinkPreview.tsx index 6a4a7b1f05..f31583fded 100644 --- a/ts/components/StoryLinkPreview.tsx +++ b/ts/components/StoryLinkPreview.tsx @@ -5,13 +5,13 @@ import React from 'react'; import classNames from 'classnames'; import { unescape } from 'lodash'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import type { LocalizerType } from '../types/Util'; import { CurveType, Image } from './conversation/Image'; import { isImageAttachment } from '../types/Attachment'; import { getSafeDomain } from '../types/LinkPreview'; -export type Props = LinkPreviewType & { +export type Props = LinkPreviewForUIType & { forceCompactMode?: boolean; i18n: LocalizerType; }; diff --git a/ts/components/TextStoryCreator.tsx b/ts/components/TextStoryCreator.tsx index 037e3d91bf..2b79ac78fe 100644 --- a/ts/components/TextStoryCreator.tsx +++ b/ts/components/TextStoryCreator.tsx @@ -8,7 +8,7 @@ import { noop } from 'lodash'; import { usePopper } from 'react-popper'; import type { EmojiPickDataType } from './emoji/EmojiPicker'; -import type { LinkPreviewType } from '../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../types/message/LinkPreviews'; import type { LocalizerType } from '../types/Util'; import type { Props as EmojiButtonPropsType } from './emoji/EmojiButton'; import type { TextAttachmentType } from '../types/Attachment'; @@ -43,7 +43,7 @@ export type PropsType = { ) => unknown; i18n: LocalizerType; isSending: boolean; - linkPreview?: LinkPreviewType; + linkPreview?: LinkPreviewForUIType; onClose: () => unknown; onDone: (textAttachment: TextAttachmentType) => unknown; onUseEmoji: (_: EmojiPickDataType) => unknown; diff --git a/ts/components/conversation/AttachmentList.stories.tsx b/ts/components/conversation/AttachmentList.stories.tsx index 92fd4677cb..3da82db2ad 100644 --- a/ts/components/conversation/AttachmentList.stories.tsx +++ b/ts/components/conversation/AttachmentList.stories.tsx @@ -6,7 +6,7 @@ import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; import type { AttachmentDraftType, - AttachmentType, + AttachmentForUIType, } from '../../types/Attachment'; import type { Props } from './AttachmentList'; import { AttachmentList } from './AttachmentList'; @@ -23,7 +23,7 @@ const { i18n } = window.SignalContext; export default { title: 'Components/Conversation/AttachmentList', -} satisfies Meta>; +} satisfies Meta>; const createProps = ( overrideProps: Partial> = {} diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index 90bcd43022..7f38f9b46f 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -1,14 +1,14 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React from 'react'; +import React, { useMemo } from 'react'; import { CurveType, Image } from './Image'; import { StagedGenericAttachment } from './StagedGenericAttachment'; import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; import type { LocalizerType } from '../../types/Util'; import type { - AttachmentType, + AttachmentForUIType, AttachmentDraftType, } from '../../types/Attachment'; import { @@ -18,15 +18,16 @@ import { isVideoAttachment, } from '../../types/Attachment'; -export type Props = Readonly<{ - attachments: ReadonlyArray; - canEditImages?: boolean; - i18n: LocalizerType; - onAddAttachment?: () => void; - onClickAttachment?: (attachment: T) => void; - onClose?: () => void; - onCloseAttachment: (attachment: T) => void; -}>; +export type Props = + Readonly<{ + attachments: ReadonlyArray; + canEditImages?: boolean; + i18n: LocalizerType; + onAddAttachment?: () => void; + onClickAttachment?: (attachment: T) => void; + onClose?: () => void; + onCloseAttachment: (attachment: T) => void; + }>; const IMAGE_WIDTH = 120; const IMAGE_HEIGHT = 120; @@ -36,7 +37,7 @@ const BLANK_VIDEO_THUMBNAIL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR42mNiAAAABgADm78GJQAAAABJRU5ErkJggg=='; function getUrl( - attachment: AttachmentType | AttachmentDraftType + attachment: AttachmentForUIType | AttachmentDraftType ): string | undefined { if (attachment.pending) { return undefined; @@ -49,7 +50,9 @@ function getUrl( return attachment.url; } -export function AttachmentList({ +export function AttachmentList< + T extends AttachmentForUIType | AttachmentDraftType, +>({ attachments, canEditImages, i18n, @@ -58,6 +61,21 @@ export function AttachmentList({ onCloseAttachment, onClose, }: Props): JSX.Element | null { + const attachmentsForUI = useMemo(() => { + return attachments.map((attachment: T): AttachmentForUIType => { + // Already ForUI attachment + if ('isPermanentlyUndownloadable' in attachment) { + return attachment; + } + + // Draft + return { + ...attachment, + isPermanentlyUndownloadable: false, + }; + }); + }, [attachments]); + if (!attachments.length) { return null; } @@ -77,8 +95,9 @@ export function AttachmentList({ ) : null}
- {(attachments || []).map((attachment, index) => { + {attachments.map((attachment, index) => { const url = getUrl(attachment); + const forUI = attachmentsForUI[index]; const key = url || attachment.path || attachment.fileName || index; @@ -106,7 +125,7 @@ export function AttachmentList({ })} className="module-staged-attachment" i18n={i18n} - attachment={attachment} + attachment={forUI} curveBottomLeft={CurveType.Tiny} curveBottomRight={CurveType.Tiny} curveTopLeft={CurveType.Tiny} diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 6a947bc864..051d7ce666 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -8,7 +8,6 @@ import type { ReadonlyDeep } from 'type-fest'; import { AddressType, ContactFormType } from '../../types/EmbeddedContact'; import { missingCaseError } from '../../util/missingCaseError'; -import { isPermanentlyUndownloadable } from '../../types/Attachment'; import { renderAvatar, renderContactShorthand, @@ -16,7 +15,7 @@ import { } from './contactUtil'; import type { - EmbeddedContactType, + EmbeddedContactForUIType, Email, Phone, PostalAddress, @@ -25,7 +24,7 @@ import type { LocalizerType } from '../../types/Util'; export type Props = { cancelAttachmentDownload: (options: { messageId: string }) => void; - contact: ReadonlyDeep; + contact: ReadonlyDeep; hasSignalAccount: boolean; i18n: LocalizerType; kickOffAttachmentDownload: (options: { messageId: string }) => void; @@ -103,7 +102,7 @@ export function ContactDetail({ if (!attachment) { return; } - if (isPermanentlyUndownloadable(attachment)) { + if (attachment.isPermanentlyUndownloadable) { return; } if (attachment.pending) { @@ -117,7 +116,7 @@ export function ContactDetail({ const attachment = contact.avatar?.avatar; const isClickable = attachment && - !isPermanentlyUndownloadable(attachment) && + !attachment.isPermanentlyUndownloadable && (attachment.pending || !attachment.path); return ( diff --git a/ts/components/conversation/EmbeddedContact.tsx b/ts/components/conversation/EmbeddedContact.tsx index 0b0f0e0f0a..5c2715a4d8 100644 --- a/ts/components/conversation/EmbeddedContact.tsx +++ b/ts/components/conversation/EmbeddedContact.tsx @@ -5,7 +5,7 @@ import React from 'react'; import classNames from 'classnames'; import type { ReadonlyDeep } from 'type-fest'; -import type { EmbeddedContactType } from '../../types/EmbeddedContact'; +import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact'; import type { LocalizerType } from '../../types/Util'; import { @@ -15,7 +15,7 @@ import { } from './contactUtil'; export type Props = { - contact: ReadonlyDeep; + contact: ReadonlyDeep; i18n: LocalizerType; isIncoming: boolean; onClick?: () => void; diff --git a/ts/components/conversation/GIF.tsx b/ts/components/conversation/GIF.tsx index 2b8a636575..e30d633aa5 100644 --- a/ts/components/conversation/GIF.tsx +++ b/ts/components/conversation/GIF.tsx @@ -13,7 +13,6 @@ import { getImageDimensions, defaultBlurHash, isDownloadable, - isPermanentlyUndownloadable, } from '../../types/Attachment'; import * as Errors from '../../types/errors'; import * as log from '../../logging/log'; @@ -273,7 +272,7 @@ export function GIF(props: Props): JSX.Element { ); - } else if (isPermanentlyUndownloadable(attachment)) { + } else if (attachment.isPermanentlyUndownloadable) { overlay = ( ) : undefined; - const isUndownloadable = isPermanentlyUndownloadable(attachment); + const isUndownloadable = attachment.isPermanentlyUndownloadable; // eslint-disable-next-line no-nested-ternary const startDownloadOrUnavailableButton = startDownload ? ( diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index ac13b1853a..16656b3fb5 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -16,7 +16,6 @@ import { getUrl, isDownloadable, isIncremental, - isPermanentlyUndownloadable, isVideoAttachment, } from '../../types/Attachment'; @@ -160,7 +159,7 @@ export function ImageGrid({ const showAttachmentOrNoLongerAvailableToast = React.useCallback( attachmentIndex => - isPermanentlyUndownloadable(attachments[attachmentIndex]) + attachments[attachmentIndex].isPermanentlyUndownloadable ? showMediaNoLongerAvailableToast : showVisualAttachment, [attachments, showVisualAttachment, showMediaNoLongerAvailableToast] diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 2e6695ff9a..ab92e4e7ab 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -45,7 +45,7 @@ import type { OwnProps as ReactionViewerProps } from './ReactionViewer'; import { ReactionViewer } from './ReactionViewer'; import { Emoji } from '../emoji/Emoji'; import { LinkPreviewDate } from './LinkPreviewDate'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews'; import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage'; import type { WidthBreakpoint } from '../_util'; import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal'; @@ -68,11 +68,10 @@ import { isGIF, isImage, isImageAttachment, - isPermanentlyUndownloadable, isPlayed, isVideo, } from '../../types/Attachment'; -import type { EmbeddedContactType } from '../../types/EmbeddedContact'; +import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact'; import { getIncrement } from '../../util/timer'; import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; @@ -250,7 +249,7 @@ export type PropsData = { timestamp: number; receivedAtMS?: number; status?: MessageStatusType; - contact?: ReadonlyDeep; + contact?: ReadonlyDeep; author: Pick< ConversationType, | 'acceptedMessageRequest' @@ -299,7 +298,7 @@ export type PropsData = { storyId?: string; text: string; }; - previews: ReadonlyArray; + previews: ReadonlyArray; isTapToView?: boolean; isTapToViewExpired?: boolean; @@ -662,7 +661,7 @@ export class Message extends React.PureComponent { if (!text && !deletedForEveryone && !attachmentDroppedDueToSize) { const firstAttachment = attachments && attachments[0]; const isAttachmentNotAvailable = - firstAttachment && isPermanentlyUndownloadable(firstAttachment); + firstAttachment?.isPermanentlyUndownloadable; if (this.isGenericAttachment(attachments, imageBroken)) { return MetadataPlacement.RenderedElsewhere; @@ -1001,7 +1000,7 @@ export class Message extends React.PureComponent { // attachmentDroppedDueToSize is handled in renderAttachmentTooBig const isAttachmentNotAvailable = - isPermanentlyUndownloadable(firstAttachment) && + firstAttachment.isPermanentlyUndownloadable && !attachmentDroppedDueToSize; if ( @@ -1391,7 +1390,7 @@ export class Message extends React.PureComponent { public renderUndownloadableTextAttachment(): JSX.Element | null { const { i18n, textAttachment, showAttachmentNotAvailableModal } = this.props; - if (!textAttachment || !isPermanentlyUndownloadable(textAttachment)) { + if (!textAttachment || !textAttachment.isPermanentlyUndownloadable) { return null; } return ( @@ -2051,7 +2050,7 @@ export class Message extends React.PureComponent { const avatarNeedsAction = attachment && !isDownloaded(attachment) && - !isPermanentlyUndownloadable(attachment); + !attachment.isPermanentlyUndownloadable; const tabIndex = otherContent || avatarNeedsAction ? 0 : -1; return ( @@ -2560,8 +2559,7 @@ export class Message extends React.PureComponent { const isViewed = readStatus === ReadStatus.Viewed; const isExpired = Boolean( !isViewed && - (isTapToViewExpired || - (firstAttachment && isPermanentlyUndownloadable(firstAttachment))) + (isTapToViewExpired || firstAttachment?.isPermanentlyUndownloadable) ); const isError = isTapToViewError || attachmentDroppedDueToSize; @@ -3044,11 +3042,7 @@ export class Message extends React.PureComponent { return; } - if ( - attachments && - attachments.length > 0 && - isPermanentlyUndownloadable(attachments[0]) - ) { + if (attachments?.[0]?.isPermanentlyUndownloadable) { event.preventDefault(); event.stopPropagation(); @@ -3162,7 +3156,7 @@ export class Message extends React.PureComponent { return; } const isAttachmentNotAvailable = - isPermanentlyUndownloadable(firstAttachment) && + firstAttachment.isPermanentlyUndownloadable && !attachmentDroppedDueToSize; if (isAttachmentNotAvailable) { @@ -3235,7 +3229,7 @@ export class Message extends React.PureComponent { (isSticker && attachments && attachments[0] && - !isPermanentlyUndownloadable(attachments[0])); + !attachments[0].isPermanentlyUndownloadable); // If it's a mostly-normal gray incoming text box, we don't want to darken it as much const lighterSelect = diff --git a/ts/components/conversation/StagedLinkPreview.tsx b/ts/components/conversation/StagedLinkPreview.tsx index aa7bbb01fa..10239cb03b 100644 --- a/ts/components/conversation/StagedLinkPreview.tsx +++ b/ts/components/conversation/StagedLinkPreview.tsx @@ -8,7 +8,7 @@ import { unescape } from 'lodash'; import { CurveType, Image } from './Image'; import { LinkPreviewDate } from './LinkPreviewDate'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews'; import type { LocalizerType } from '../../types/Util'; import { getClassNamesFor } from '../../util/getClassNamesFor'; import { isImageAttachment } from '../../types/Attachment'; @@ -17,7 +17,7 @@ import { Avatar } from '../Avatar'; import { getColorForCallLink } from '../../util/getColorForCallLink'; import { getKeyFromCallLink } from '../../util/callLinks'; -export type Props = LinkPreviewType & { +export type Props = LinkPreviewForUIType & { i18n: LocalizerType; imageSize?: number; moduleClassName?: string; diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 07646ec2ed..db01819375 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -363,6 +363,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ contentType: LONG_MESSAGE, size: 123, pending: false, + isPermanentlyUndownloadable: false, }, theme: ThemeType.light, timestamp: overrideProps.timestamp ?? Date.now(), @@ -551,6 +552,7 @@ Pending.args = { contentType: LONG_MESSAGE, size: 123, pending: true, + isPermanentlyUndownloadable: false, }, }; @@ -564,6 +566,7 @@ LongBodyCanBeDownloaded.args = { error: true, digest: 'abc', key: 'def', + isPermanentlyUndownloadable: false, }, }; @@ -861,6 +864,7 @@ const bigAttachment = { id: undefined, error: true, wasTooBig: true, + isPermanentlyUndownloadable: true, }; export function AttachmentTooBig(): JSX.Element { @@ -2319,6 +2323,7 @@ EmbeddedContactAvatarPermanentError.args = { id: undefined, key: undefined, error: true, + isPermanentlyUndownloadable: true, path: undefined, contentType: IMAGE_GIF, size: 1000000, @@ -2539,6 +2544,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2555,6 +2561,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), fakeAttachment({ contentType: IMAGE_JPEG, @@ -2566,6 +2573,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2583,6 +2591,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2599,6 +2608,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2614,6 +2624,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2633,6 +2644,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { key: undefined, id: undefined, error: true, + isPermanentlyUndownloadable: true, }), ], status: 'sent', @@ -2649,6 +2661,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { width: 128, height: 128, error: true, + isPermanentlyUndownloadable: true, }), ], isSticker: true, @@ -2662,6 +2675,7 @@ export function PermanentlyUndownloadableAttachments(): JSX.Element { pending: false, key: undefined, error: true, + isPermanentlyUndownloadable: true, }, }); diff --git a/ts/components/conversation/contactUtil.tsx b/ts/components/conversation/contactUtil.tsx index 50e9a13f4c..2334a4ab67 100644 --- a/ts/components/conversation/contactUtil.tsx +++ b/ts/components/conversation/contactUtil.tsx @@ -11,8 +11,7 @@ import { getName } from '../../types/EmbeddedContact'; import { AttachmentStatusIcon } from './AttachmentStatusIcon'; import type { LocalizerType } from '../../types/Util'; -import type { EmbeddedContactType } from '../../types/EmbeddedContact'; -import { isPermanentlyUndownloadable } from '../../types/Attachment'; +import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact'; export function renderAvatar({ contact, @@ -20,7 +19,7 @@ export function renderAvatar({ i18n, size, }: { - contact: ReadonlyDeep; + contact: ReadonlyDeep; direction?: 'outgoing' | 'incoming'; i18n: LocalizerType; size: 52 | 80; @@ -30,7 +29,7 @@ export function renderAvatar({ const avatarUrl = avatar && avatar.avatar && avatar.avatar.path; const title = getName(contact) || ''; const isAttachmentNotAvailable = Boolean( - avatar?.avatar && isPermanentlyUndownloadable(avatar?.avatar) + avatar?.avatar?.isPermanentlyUndownloadable ); const renderAttachmentDownloaded = () => ( @@ -64,7 +63,7 @@ export function renderName({ isIncoming, module, }: { - contact: ReadonlyDeep; + contact: ReadonlyDeep; isIncoming: boolean; module: string; }): JSX.Element { @@ -85,7 +84,7 @@ export function renderContactShorthand({ isIncoming, module, }: { - contact: ReadonlyDeep; + contact: ReadonlyDeep; isIncoming: boolean; module: string; }): JSX.Element { diff --git a/ts/jobs/AttachmentDownloadManager.ts b/ts/jobs/AttachmentDownloadManager.ts index 34522f1058..4f35912456 100644 --- a/ts/jobs/AttachmentDownloadManager.ts +++ b/ts/jobs/AttachmentDownloadManager.ts @@ -58,7 +58,12 @@ import { deleteDownloadsJobQueue } from './deleteDownloadsJobQueue'; import { createBatcher } from '../util/batcher'; import { showDownloadFailedToast } from '../util/showDownloadFailedToast'; import { markAttachmentAsPermanentlyErrored } from '../util/attachments/markAttachmentAsPermanentlyErrored'; -import { AttachmentBackfill } from './helpers/attachmentBackfill'; +import { + AttachmentBackfill, + isPermanentlyUndownloadable, +} from './helpers/attachmentBackfill'; + +export { isPermanentlyUndownloadable }; // Type for adding a new job export type NewAttachmentDownloadJobType = { @@ -424,21 +429,23 @@ async function runDownloadAttachmentJob({ } if (error instanceof AttachmentPermanentlyUndownloadableError) { - if ( + const canBackfill = job.isManualDownload && - job.source !== AttachmentDownloadSource.BACKFILL && AttachmentBackfill.isEnabledForJob( job.attachmentType, message.attributes - ) - ) { + ); + + if (job.source !== AttachmentDownloadSource.BACKFILL && canBackfill) { await AttachmentDownloadManager.requestBackfill(message.attributes); return { status: 'finished' }; } await addAttachmentToMessage( message.id, - markAttachmentAsPermanentlyErrored(job.attachment), + markAttachmentAsPermanentlyErrored(job.attachment, { + backfillError: false, + }), logId, { type: job.attachmentType } ); @@ -733,7 +740,9 @@ async function downloadBackupThumbnail({ function _markAttachmentAsTooBig(attachment: AttachmentType): AttachmentType { return { - ...markAttachmentAsPermanentlyErrored(attachment), + ...markAttachmentAsPermanentlyErrored(attachment, { + backfillError: false, + }), wasTooBig: true, }; } diff --git a/ts/jobs/helpers/attachmentBackfill.ts b/ts/jobs/helpers/attachmentBackfill.ts index fcef6c125a..e8ead6f5db 100644 --- a/ts/jobs/helpers/attachmentBackfill.ts +++ b/ts/jobs/helpers/attachmentBackfill.ts @@ -9,6 +9,7 @@ import { type AttachmentType, isDownloading, isDownloaded, + isDownloadable, } from '../../types/Attachment'; import { type AttachmentDownloadJobTypeType, @@ -191,7 +192,9 @@ export class AttachmentBackfill { changeCount += 1; updatedSticker = { ...updatedSticker, - data: markAttachmentAsPermanentlyErrored(existing), + data: markAttachmentAsPermanentlyErrored(existing, { + backfillError: true, + }), }; showToast = true; } else { @@ -240,7 +243,8 @@ export class AttachmentBackfill { } else if (response.longText.status === Status.TERMINAL_ERROR) { changeCount += 1; updatedBodyAttachment = markAttachmentAsPermanentlyErrored( - updatedBodyAttachment + updatedBodyAttachment, + { backfillError: true } ); showToast = true; } else { @@ -272,8 +276,10 @@ export class AttachmentBackfill { showToast = true; changeCount += 1; - updatedAttachments[index] = - markAttachmentAsPermanentlyErrored(existing); + updatedAttachments[index] = markAttachmentAsPermanentlyErrored( + existing, + { backfillError: true } + ); } else { throw missingCaseError(entry.status); } @@ -432,3 +438,28 @@ export class AttachmentBackfill { }); } } + +export function isPermanentlyUndownloadable( + attachment: AttachmentType, + disposition: AttachmentDownloadJobTypeType, + message: Pick +): boolean { + // Attachment is downloadable or user have not failed to download it yet + if (isDownloadable(attachment) || !attachment.error) { + return false; + } + + // Too big attachments cannot be retried anymore + if (attachment.wasTooBig) { + return true; + } + + // Previous backfill failed + if (attachment.backfillError) { + return true; + } + + // If backfill is unavailable for the attachment - it cannot be downloaded + // at this time. + return !AttachmentBackfill.isEnabledForJob(disposition, message); +} diff --git a/ts/services/storyLoader.ts b/ts/services/storyLoader.ts index 6863fab5ba..2c5ee2e818 100644 --- a/ts/services/storyLoader.ts +++ b/ts/services/storyLoader.ts @@ -71,12 +71,14 @@ export function getStoryDataFromMessageAttributes( ...attachment.textAttachment, preview: { ...preview, - image: preview.image && getPropsForAttachment(preview.image), + image: + preview.image && + getPropsForAttachment(preview.image, 'preview', message), }, }, }; } else if (attachment) { - attachment = getPropsForAttachment(attachment); + attachment = getPropsForAttachment(attachment, 'attachment', message); } // for a story, the message should always include the sourceDevice diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 3745624681..545743485e 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -22,7 +22,7 @@ import { import { DataReader, DataWriter } from '../../sql/Client'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { DraftBodyRanges } from '../../types/BodyRange'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews'; import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import type { NoopActionType } from './noop'; import type { ShowToastActionType } from './toast'; @@ -103,7 +103,7 @@ type ComposerStateByConversationType = { focusCounter: number; isDisabled: boolean; linkPreviewLoading: boolean; - linkPreviewResult?: LinkPreviewType; + linkPreviewResult?: LinkPreviewForUIType; messageCompositionId: string; quotedMessage?: QuotedMessageForComposerType; sendCounter: number; diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 98ef02b7ce..1ef137a6b2 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -23,7 +23,10 @@ import * as SingleServePromise from '../../services/singleServePromise'; import * as Stickers from '../../types/Stickers'; import { UsernameOnboardingState } from '../../types/globalModals'; import * as log from '../../logging/log'; -import { getMessagePropsSelector } from '../selectors/message'; +import { + getMessagePropsSelector, + getPropsForAttachment, +} from '../selectors/message'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper'; import { useBoundActions } from '../../hooks/useBoundActions'; @@ -37,10 +40,8 @@ import { MESSAGE_EXPIRED, actions as conversationsActions, } from './conversations'; -import { - isDownloaded, - isPermanentlyUndownloadable, -} from '../../types/Attachment'; +import { isDownloaded } from '../../types/Attachment'; +import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager'; import type { ButtonVariant } from '../../components/Button'; import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation'; import type { MessageForwardDraft } from '../../types/ForwardDraft'; @@ -782,7 +783,11 @@ function toggleForwardMessagesModal( !attachments.every( attachment => isDownloaded(attachment) || - isPermanentlyUndownloadable(attachment) + isPermanentlyUndownloadable( + attachment, + 'attachment', + message.attributes + ) ) ) { dispatch( @@ -799,7 +804,12 @@ function toggleForwardMessagesModal( { ...messageProps, attachments: (messageProps.attachments ?? []).filter( - attachment => !isPermanentlyUndownloadable(attachment) + attachment => + !isPermanentlyUndownloadable( + attachment, + 'attachment', + message.attributes + ) ), }, conversationSelector @@ -1207,7 +1217,9 @@ function copyOverMessageAttributesIntoForwardMessages( } return { ...messageDraft, - attachments: attributes.attachments, + attachments: attributes.attachments?.map(attachment => + getPropsForAttachment(attachment, 'attachment', attributes) + ), }; }); } diff --git a/ts/state/ducks/linkPreviews.ts b/ts/state/ducks/linkPreviews.ts index b9a4d5e92f..74e2a1f840 100644 --- a/ts/state/ducks/linkPreviews.ts +++ b/ts/state/ducks/linkPreviews.ts @@ -5,7 +5,11 @@ import type { ThunkAction } from 'redux-thunk'; import type { ReadonlyDeep } from 'type-fest'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { + LinkPreviewType, + LinkPreviewForUIType, +} from '../../types/message/LinkPreviews'; +import type { AttachmentForUIType } from '../../types/Attachment'; import type { MaybeGrabLinkPreviewOptionsType } from '../../types/LinkPreview'; import type { NoopActionType } from './noop'; import type { StateType as RootStateType } from '../reducer'; @@ -14,11 +18,12 @@ import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnece import { maybeGrabLinkPreview } from '../../services/LinkPreview'; import { strictAssert } from '../../util/assert'; import { useBoundActions } from '../../hooks/useBoundActions'; +import { getPropsForAttachment } from '../selectors/message'; // State export type LinkPreviewsStateType = ReadonlyDeep<{ - linkPreview?: LinkPreviewType; + linkPreview?: LinkPreviewForUIType; source?: LinkPreviewSourceType; }>; @@ -31,7 +36,7 @@ export type AddLinkPreviewActionType = ReadonlyDeep<{ type: 'linkPreviews/ADD_PREVIEW'; payload: { conversationId?: string; - linkPreview: LinkPreviewType; + linkPreview: LinkPreviewForUIType; source: LinkPreviewSourceType; }; }>; @@ -73,11 +78,27 @@ function addLinkPreview( strictAssert(conversationId, 'no conversationId provided'); } + let image: AttachmentForUIType | undefined; + if (linkPreview.image != null) { + image = { + ...getPropsForAttachment(linkPreview.image, 'preview', { + type: + source === LinkPreviewSourceType.StoryCreator ? 'story' : 'outgoing', + }), + + // Save URL to the blob (it gets stripped by `getPropsForAttachment`) + url: linkPreview.image.url, + }; + } + return { type: ADD_PREVIEW, payload: { conversationId, - linkPreview, + linkPreview: { + ...linkPreview, + image, + }, source, }, }; diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index da935c1572..921ed298c3 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -54,12 +54,12 @@ import type { ServiceIdString, } from '../../types/ServiceId'; -import type { EmbeddedContactType } from '../../types/EmbeddedContact'; +import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact'; import { embeddedContactSelector } from '../../types/EmbeddedContact'; import type { HydratedBodyRangesType } from '../../types/BodyRange'; import { hydrateRanges } from '../../types/BodyRange'; import type { AssertProps } from '../../types/Util'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews'; import { getMentionsRegex } from '../../types/Message'; import { SignalService as Proto } from '../../protobuf'; import type { @@ -67,6 +67,7 @@ import type { AttachmentType, } from '../../types/Attachment'; import { isVoiceMessage, defaultBlurHash } from '../../types/Attachment'; +import type { AttachmentDownloadJobTypeType } from '../../types/AttachmentDownload'; import { type DefaultConversationColorType } from '../../types/Colors'; import { ReadStatus } from '../../messages/MessageReadStatus'; @@ -79,6 +80,7 @@ import * as iterables from '../../util/iterables'; import { strictAssert } from '../../util/assert'; import { canEditMessage } from '../../util/canEditMessage'; import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl'; +import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager'; import { getAccountSelector } from './accounts'; import { getDefaultConversationColor } from './items'; @@ -300,10 +302,10 @@ export function getConversation( // Message -export const getAttachmentsForMessage = ({ - sticker, - attachments = [], -}: MessageWithUIFieldsType): Array => { +export const getAttachmentsForMessage = ( + message: MessageWithUIFieldsType +): Array => { + const { sticker, attachments = [] } = message; if (sticker && sticker.data) { const { data } = sticker; @@ -324,7 +326,9 @@ export const getAttachmentsForMessage = ({ // 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) - .map(attachment => getPropsForAttachment(attachment)) + .map(attachment => + getPropsForAttachment(attachment, 'attachment', message) + ) .filter(isNotNil) ); }; @@ -383,15 +387,18 @@ const getAuthorForMessage = ( return safe; }; -const getPreviewsForMessage = ({ - preview: previews = [], -}: MessageWithUIFieldsType): Array => { +const getPreviewsForMessage = ( + message: MessageWithUIFieldsType +): Array => { + const { preview: previews = [] } = message; return previews.map(preview => ({ ...preview, isStickerPack: isStickerPack(preview.url), isCallLink: isCallLink(preview.url), domain: getSafeDomain(preview.url), - image: preview.image ? getPropsForAttachment(preview.image) : undefined, + image: preview.image + ? getPropsForAttachment(preview.image, 'preview', message) + : undefined, })); }; @@ -596,7 +603,8 @@ function getTextAttachment( message: MessageWithUIFieldsType ): AttachmentType | undefined { return ( - message.bodyAttachment && getPropsForAttachment(message.bodyAttachment) + message.bodyAttachment && + getPropsForAttachment(message.bodyAttachment, 'long-message', message) ); } @@ -725,7 +733,9 @@ export const getPropsForMessage = ( ); return { - attachments, + attachments: attachments?.map(attachment => + getPropsForAttachment(attachment, 'attachment', message) + ), attachmentDroppedDueToSize, author, bodyRanges, @@ -734,7 +744,10 @@ export const getPropsForMessage = ( quote, reactions, storyReplyContext, - textAttachment, + textAttachment: + textAttachment == null + ? undefined + : getPropsForAttachment(textAttachment, 'long-message', message), payment, canCopy: canCopy(message), canEditMessage: canEditMessage(message), @@ -1810,7 +1823,7 @@ export function getPropsForEmbeddedContact( message: MessageWithUIFieldsType, regionCode: string | undefined, accountSelector: (identifier?: string) => ServiceIdString | undefined -): ReadonlyDeep | undefined { +): ReadonlyDeep | undefined { const contacts = message.contact; if (!contacts || !contacts.length) { return undefined; @@ -1828,12 +1841,10 @@ export function getPropsForEmbeddedContact( } export function getPropsForAttachment( - attachment: AttachmentType -): AttachmentForUIType | undefined { - if (!attachment) { - return undefined; - } - + attachment: AttachmentType, + disposition: AttachmentDownloadJobTypeType, + message: Pick +): AttachmentForUIType { const { path, pending, screenshot, thumbnail, thumbnailFromBackup } = attachment; @@ -1865,6 +1876,11 @@ export function getPropsForAttachment( url: getLocalAttachmentUrl(thumbnail), } : undefined, + isPermanentlyUndownloadable: isPermanentlyUndownloadable( + attachment, + disposition, + message + ), }; } diff --git a/ts/test-both/helpers/fakeAttachment.ts b/ts/test-both/helpers/fakeAttachment.ts index 12d974843e..81ddac7f42 100644 --- a/ts/test-both/helpers/fakeAttachment.ts +++ b/ts/test-both/helpers/fakeAttachment.ts @@ -19,6 +19,7 @@ export const fakeAttachment = ( size: 10304, // This is to get rid of the download buttons on most of our stories path: 'ab/ablahblahblah', + isPermanentlyUndownloadable: false, ...overrides, }); diff --git a/ts/test-both/state/ducks/linkPreviews_test.ts b/ts/test-both/state/ducks/linkPreviews_test.ts index e8af013bd6..dae781e505 100644 --- a/ts/test-both/state/ducks/linkPreviews_test.ts +++ b/ts/test-both/state/ducks/linkPreviews_test.ts @@ -8,13 +8,14 @@ import { getEmptyState, reducer, } from '../../../state/ducks/linkPreviews'; -import type { LinkPreviewType } from '../../../types/message/LinkPreviews'; +import type { LinkPreviewForUIType } from '../../../types/message/LinkPreviews'; describe('both/state/ducks/linkPreviews', () => { - function getMockLinkPreview(): LinkPreviewType { + function getMockLinkPreview(): LinkPreviewForUIType { return { title: 'Hello World', domain: 'signal.org', + image: undefined, url: 'https://www.signal.org', isStickerPack: false, isCallLink: false, @@ -29,7 +30,7 @@ describe('both/state/ducks/linkPreviews', () => { const linkPreview = getMockLinkPreview(); const nextState = reducer(state, addLinkPreview(linkPreview, 1)); - assert.strictEqual(nextState.linkPreview, linkPreview); + assert.deepEqual(nextState.linkPreview, linkPreview); }); }); diff --git a/ts/test-electron/state/ducks/stories_test.ts b/ts/test-electron/state/ducks/stories_test.ts index 75d7150249..3c6fd85d90 100644 --- a/ts/test-electron/state/ducks/stories_test.ts +++ b/ts/test-electron/state/ducks/stories_test.ts @@ -969,6 +969,7 @@ describe('both/state/ducks/stories', () => { contentType: IMAGE_JPEG, digest: 'digest-1', size: 0, + isPermanentlyUndownloadable: false, }, isCallLink: false, }; diff --git a/ts/test-mock/messaging/backfill_test.ts b/ts/test-mock/messaging/backfill_test.ts index 0b3c1db39f..bb526e12e8 100644 --- a/ts/test-mock/messaging/backfill_test.ts +++ b/ts/test-mock/messaging/backfill_test.ts @@ -225,6 +225,10 @@ describe('attachment backfill', function (this: Mocha.Suite) { name: 'This media is not available', }) .waitFor(); + + await conversationStack + .locator('.module-image__undownloadable-icon') + .waitFor(); }); it('should show modal on timeout', async () => { diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 928a574e7d..31645d4035 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -13,7 +13,7 @@ import { } from 'lodash'; import { blobToArrayBuffer } from 'blob-util'; -import type { LinkPreviewType } from './message/LinkPreviews'; +import type { LinkPreviewForUIType } from './message/LinkPreviews'; import type { LoggerType } from './Logging'; import * as logging from '../logging/log'; import * as MIME from './MIME'; @@ -89,6 +89,9 @@ export type AttachmentType = { textAttachment?: TextAttachmentType; wasTooBig?: boolean; + // If `true` backfill is unavailable + backfillError?: boolean; + totalDownloaded?: number; incrementalMac?: string; chunkSize?: number; @@ -141,6 +144,7 @@ export type AddressableAttachmentType = Readonly<{ }>; export type AttachmentForUIType = AttachmentType & { + isPermanentlyUndownloadable: boolean; thumbnailFromBackup?: { url?: string; }; @@ -177,7 +181,7 @@ export type TextAttachmentType = { textStyle?: number | null; textForegroundColor?: number | null; textBackgroundColor?: number | null; - preview?: LinkPreviewType; + preview?: LinkPreviewForUIType; gradient?: { startColor?: number | null; endColor?: number | null; @@ -1284,12 +1288,6 @@ export function isDownloadable(attachment: AttachmentType): boolean { ); } -export function isPermanentlyUndownloadable( - attachment: AttachmentType -): boolean { - return Boolean(!isDownloadable(attachment) && attachment.error); -} - export function isAttachmentLocallySaved( attachment: AttachmentType ): attachment is LocallySavedAttachment { diff --git a/ts/types/EmbeddedContact.ts b/ts/types/EmbeddedContact.ts index ccc5ef32f5..4a8c94b040 100644 --- a/ts/types/EmbeddedContact.ts +++ b/ts/types/EmbeddedContact.ts @@ -14,6 +14,7 @@ import { } from './PhoneNumber'; import type { AttachmentType, + AttachmentForUIType, AttachmentWithHydratedData, LocalAttachmentV2Type, UploadedAttachmentType, @@ -38,6 +39,7 @@ type GenericEmbeddedContactType = { }; export type EmbeddedContactType = GenericEmbeddedContactType; +export type EmbeddedContactForUIType = GenericEmbeddedContactType; export type EmbeddedContactWithHydratedAvatar = GenericEmbeddedContactType; export type EmbeddedContactWithUploadedAvatar = @@ -95,6 +97,7 @@ type GenericAvatar = { }; export type Avatar = GenericAvatar; +export type AvatarForUI = GenericAvatar; export type AvatarWithHydratedData = GenericAvatar; export type UploadedAvatar = GenericAvatar; @@ -154,21 +157,25 @@ export function embeddedContactSelector( firstNumber?: string; serviceId?: ServiceIdString; } -): ReadonlyDeep { +): ReadonlyDeep { const { firstNumber, serviceId, regionCode } = options; - let { avatar } = contact; + const { avatar } = contact; + let avatarForUI: EmbeddedContactForUIType['avatar']; if (avatar && avatar.avatar) { if (avatar.avatar.error) { - avatar = undefined; + avatarForUI = undefined; } else { - avatar = { + avatarForUI = { ...avatar, avatar: { ...avatar.avatar, path: avatar.avatar.path ? getLocalAttachmentUrl(avatar.avatar) : undefined, + + // `error` case is handled above + isPermanentlyUndownloadable: false, }, }; } @@ -178,7 +185,7 @@ export function embeddedContactSelector( ...contact, firstNumber, serviceId, - avatar, + avatar: avatarForUI, number: contact.number && contact.number.map(item => ({ diff --git a/ts/types/ForwardDraft.ts b/ts/types/ForwardDraft.ts index 6b8b70f051..ba035e81b3 100644 --- a/ts/types/ForwardDraft.ts +++ b/ts/types/ForwardDraft.ts @@ -5,20 +5,20 @@ import { orderBy } from 'lodash'; import type { ReadonlyMessageAttributesType } from '../model-types'; import { isVoiceMessage, - type AttachmentType, + type AttachmentForUIType, isDownloaded, } from './Attachment'; import type { HydratedBodyRangesType } from './BodyRange'; -import type { LinkPreviewType } from './message/LinkPreviews'; +import type { LinkPreviewForUIType } from './message/LinkPreviews'; export type MessageForwardDraft = Readonly<{ - attachments?: ReadonlyArray; + attachments?: ReadonlyArray; bodyRanges?: HydratedBodyRangesType; hasContact: boolean; isSticker: boolean; messageBody?: string; originalMessageId: string | null; // null for new messages - previews: ReadonlyArray; + previews: ReadonlyArray; }>; export type ForwardMessageData = Readonly<{ diff --git a/ts/types/message/LinkPreviews.ts b/ts/types/message/LinkPreviews.ts index 31d6263080..371ff26cac 100644 --- a/ts/types/message/LinkPreviews.ts +++ b/ts/types/message/LinkPreviews.ts @@ -1,7 +1,11 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { AttachmentType, AttachmentWithHydratedData } from '../Attachment'; +import type { + AttachmentType, + AttachmentForUIType, + AttachmentWithHydratedData, +} from '../Attachment'; type GenericLinkPreviewType = { title?: string; @@ -16,6 +20,7 @@ type GenericLinkPreviewType = { }; export type LinkPreviewType = GenericLinkPreviewType; +export type LinkPreviewForUIType = GenericLinkPreviewType; export type LinkPreviewWithHydratedData = GenericLinkPreviewType; diff --git a/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts b/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts index c0d3183565..553a860a11 100644 --- a/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts +++ b/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts @@ -6,7 +6,13 @@ import { omit } from 'lodash'; import { type AttachmentType } from '../../types/Attachment'; export function markAttachmentAsPermanentlyErrored( - attachment: AttachmentType + attachment: AttachmentType, + { backfillError }: { backfillError: boolean } ): AttachmentType { - return { ...omit(attachment, ['key', 'id']), pending: false, error: true }; + return { + ...omit(attachment, ['key', 'id']), + pending: false, + error: true, + backfillError, + }; }