From a024ee4b968de0ff9977440808acca6b5604cbf8 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 15 Nov 2021 13:54:33 -0800 Subject: [PATCH] Allow stage and send of video, even if we can't get screenshot --- ts/components/CompositionArea.stories.tsx | 4 +- ts/components/CompositionArea.tsx | 21 ++++++---- ts/components/CompositionUpload.tsx | 11 +++-- ts/components/ForwardMessageModal.stories.tsx | 31 +++++++------- ts/components/ForwardMessageModal.tsx | 12 +++--- .../conversation/AttachmentList.stories.tsx | 40 +++++++------------ ts/components/conversation/AttachmentList.tsx | 34 +++++++++++----- ts/components/conversation/AudioCapture.tsx | 9 +++-- ts/components/conversation/Image.tsx | 6 ++- ts/model-types.d.ts | 8 +++- ts/state/ducks/audioRecorder.ts | 7 ++-- ts/state/ducks/composer.ts | 23 ++++++----- ts/state/smart/ForwardMessageModal.tsx | 6 +-- ts/test-both/helpers/fakeAttachment.ts | 15 ++++++- ts/test-both/state/ducks/composer_test.ts | 17 +++++--- ts/types/Attachment.ts | 26 ++++++++---- ts/util/handleAttachmentsProcessing.ts | 11 +++-- ts/util/processAttachment.ts | 17 +++++--- ...isk.ts => resolveDraftAttachmentOnDisk.ts} | 11 ++--- ts/util/writeDraftAttachment.ts | 34 +++++++++------- ts/views/conversation_view.ts | 24 +++++++++-- 21 files changed, 224 insertions(+), 143 deletions(-) rename ts/util/{resolveAttachmentOnDisk.ts => resolveDraftAttachmentOnDisk.ts} (71%) diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 1b97182fda..cdb30da77d 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -13,7 +13,7 @@ import { CompositionArea } from './CompositionArea'; import { setupI18n } from '../util/setupI18n'; import enMessages from '../../_locales/en/messages.json'; -import { fakeAttachment } from '../test-both/helpers/fakeAttachment'; +import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment'; import { landscapeGreenUrl } from '../storybook/Fixtures'; import { ThemeType } from '../types/Util'; import { RecordingState } from '../state/ducks/audioRecorder'; @@ -165,7 +165,7 @@ story.add('SMS-only', () => { story.add('Attachments', () => { const props = createProps({ draftAttachments: [ - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_JPEG, url: landscapeGreenUrl, }), diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index a7cf1860c4..7a27fa59ef 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -34,7 +34,10 @@ import type { PropsType as GroupV2PendingApprovalActionsPropsType } from './conv import { GroupV2PendingApprovalActions } from './conversation/GroupV2PendingApprovalActions'; import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner'; import { AttachmentList } from './conversation/AttachmentList'; -import type { AttachmentType } from '../types/Attachment'; +import type { + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../types/Attachment'; import { isImageAttachment } from '../types/Attachment'; import { AudioCapture } from './conversation/AudioCapture'; import { CompositionUpload } from './CompositionUpload'; @@ -67,11 +70,11 @@ export type OwnProps = Readonly<{ acceptedMessageRequest?: boolean; addAttachment: ( conversationId: string, - attachment: AttachmentType + attachment: InMemoryAttachmentDraftType ) => unknown; addPendingAttachment: ( conversationId: string, - pendingAttachment: AttachmentType + pendingAttachment: AttachmentDraftType ) => unknown; announcementsOnly?: boolean; areWeAdmin?: boolean; @@ -80,11 +83,11 @@ export type OwnProps = Readonly<{ cancelRecording: () => unknown; completeRecording: ( conversationId: string, - onSendAudioRecording?: (rec: AttachmentType) => unknown + onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown ) => unknown; compositionApi?: MutableRefObject; conversationId: string; - draftAttachments: ReadonlyArray; + draftAttachments: ReadonlyArray; errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType; errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; groupAdmins: Array; @@ -105,11 +108,11 @@ export type OwnProps = Readonly<{ processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown; onSelectMediaQuality(isHQ: boolean): unknown; onSendMessage(options: { - draftAttachments?: ReadonlyArray; + draftAttachments?: ReadonlyArray; mentions?: BodyRangesType; message?: string; timestamp?: number; - voiceNoteAttachment?: AttachmentType; + voiceNoteAttachment?: InMemoryAttachmentDraftType; }): unknown; openConversation(conversationId: string): unknown; quotedMessageProps?: Omit< @@ -373,7 +376,9 @@ export const CompositionArea = ({ errorRecording={errorRecording} i18n={i18n} recordingState={recordingState} - onSendAudioRecording={(voiceNoteAttachment: AttachmentType) => { + onSendAudioRecording={( + voiceNoteAttachment: InMemoryAttachmentDraftType + ) => { onSendMessage({ voiceNoteAttachment }); }} startRecording={startRecording} diff --git a/ts/components/CompositionUpload.tsx b/ts/components/CompositionUpload.tsx index 718012d951..b08c211802 100644 --- a/ts/components/CompositionUpload.tsx +++ b/ts/components/CompositionUpload.tsx @@ -4,7 +4,10 @@ import type { ChangeEventHandler } from 'react'; import React, { forwardRef, useState } from 'react'; -import type { AttachmentType } from '../types/Attachment'; +import type { + InMemoryAttachmentDraftType, + AttachmentDraftType, +} from '../types/Attachment'; import { AttachmentToastType } from '../types/AttachmentToastType'; import type { LocalizerType } from '../types/Util'; @@ -19,14 +22,14 @@ import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachme export type PropsType = { addAttachment: ( conversationId: string, - attachment: AttachmentType + attachment: InMemoryAttachmentDraftType ) => unknown; addPendingAttachment: ( conversationId: string, - pendingAttachment: AttachmentType + pendingAttachment: AttachmentDraftType ) => unknown; conversationId: string; - draftAttachments: ReadonlyArray; + draftAttachments: ReadonlyArray; i18n: LocalizerType; processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown; removeAttachment: (conversationId: string, filePath: string) => unknown; diff --git a/ts/components/ForwardMessageModal.stories.tsx b/ts/components/ForwardMessageModal.stories.tsx index f80ed06907..95f0d5fce1 100644 --- a/ts/components/ForwardMessageModal.stories.tsx +++ b/ts/components/ForwardMessageModal.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import { text } from '@storybook/addon-knobs'; import enMessages from '../../_locales/en/messages.json'; -import type { AttachmentType } from '../types/Attachment'; +import type { AttachmentDraftType } from '../types/Attachment'; import type { PropsType } from './ForwardMessageModal'; import { ForwardMessageModal } from './ForwardMessageModal'; import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME'; @@ -16,15 +16,17 @@ import { getDefaultConversation } from '../test-both/helpers/getDefaultConversat import { setupI18n } from '../util/setupI18n'; import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; -const createAttachment = ( - props: Partial = {} -): AttachmentType => ({ +const createDraftAttachment = ( + props: Partial = {} +): AttachmentDraftType => ({ + pending: false, + path: 'fileName.jpg', contentType: stringToMIMEType( text('attachment contentType', props.contentType || '') ), fileName: text('attachment fileName', props.fileName || ''), - screenshot: props.screenshot, - url: text('attachment url', props.url || ''), + screenshotPath: props.pending === false ? props.screenshotPath : undefined, + url: text('attachment url', props.pending === false ? props.url || '' : ''), size: 3433, }); @@ -81,7 +83,7 @@ story.add('link preview', () => { date: Date.now(), domain: 'https://www.signal.org', url: 'signal.org', - image: createAttachment({ + image: createDraftAttachment({ url: '/fixtures/kitten-4-112-112.jpg', contentType: IMAGE_JPEG, }), @@ -99,22 +101,19 @@ story.add('media attachments', () => { ; + attachments?: Array; candidateConversations: ReadonlyArray; conversationId: string; doForwardMessage: ( selectedContacts: Array, messageBody?: string, - attachments?: Array, + attachments?: Array, linkPreview?: LinkPreviewType ) => void; i18n: LocalizerType; @@ -100,7 +100,9 @@ export const ForwardMessageModal: FunctionComponent = ({ const [filteredConversations, setFilteredConversations] = useState( filterAndSortConversationsByRecent(candidateConversations, '') ); - const [attachmentsToForward, setAttachmentsToForward] = useState(attachments); + const [attachmentsToForward, setAttachmentsToForward] = useState< + Array + >(attachments || []); const [isEditingMessage, setIsEditingMessage] = useState(false); const [messageBodyText, setMessageBodyText] = useState(messageBody || ''); const [cannotMessage, setCannotMessage] = useState(false); @@ -322,7 +324,7 @@ export const ForwardMessageModal: FunctionComponent = ({ { + onCloseAttachment={(attachment: AttachmentDraftType) => { const newAttachments = attachmentsToForward.filter( currentAttachment => currentAttachment !== attachment ); diff --git a/ts/components/conversation/AttachmentList.stories.tsx b/ts/components/conversation/AttachmentList.stories.tsx index 6c1341701a..30c81bd078 100644 --- a/ts/components/conversation/AttachmentList.stories.tsx +++ b/ts/components/conversation/AttachmentList.stories.tsx @@ -18,7 +18,7 @@ import { import { setupI18n } from '../../util/setupI18n'; import enMessages from '../../../_locales/en/messages.json'; -import { fakeAttachment } from '../../test-both/helpers/fakeAttachment'; +import { fakeDraftAttachment } from '../../test-both/helpers/fakeAttachment'; const i18n = setupI18n('en', enMessages); @@ -36,7 +36,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ story.add('One File', () => { const props = createProps({ attachments: [ - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_JPEG, fileName: 'tina-rolf-269345-unsplash.jpg', url: '/fixtures/tina-rolf-269345-unsplash.jpg', @@ -49,24 +49,18 @@ story.add('One File', () => { story.add('Multiple Visual Attachments', () => { const props = createProps({ attachments: [ - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_JPEG, fileName: 'tina-rolf-269345-unsplash.jpg', url: '/fixtures/tina-rolf-269345-unsplash.jpg', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: VIDEO_MP4, fileName: 'pixabay-Soap-Bubble-7141.mp4', - url: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - screenshot: { - height: 112, - width: 112, - url: '/fixtures/kitten-4-112-112.jpg', - contentType: IMAGE_JPEG, - path: 'originalpath', - }, + url: '/fixtures/kitten-4-112-112.jpg', + screenshotPath: '/fixtures/kitten-4-112-112.jpg', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_GIF, fileName: 'giphy-GVNv0UpeYm17e', url: '/fixtures/giphy-GVNvOUpeYmI7e.gif', @@ -80,34 +74,28 @@ story.add('Multiple Visual Attachments', () => { story.add('Multiple with Non-Visual Types', () => { const props = createProps({ attachments: [ - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_JPEG, fileName: 'tina-rolf-269345-unsplash.jpg', url: '/fixtures/tina-rolf-269345-unsplash.jpg', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: stringToMIMEType('text/plain'), fileName: 'lorem-ipsum.txt', url: '/fixtures/lorem-ipsum.txt', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: AUDIO_MP3, fileName: 'incompetech-com-Agnus-Dei-X.mp3', url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: VIDEO_MP4, fileName: 'pixabay-Soap-Bubble-7141.mp4', - url: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - screenshot: { - height: 112, - width: 112, - url: '/fixtures/kitten-4-112-112.jpg', - contentType: IMAGE_JPEG, - path: 'originalpath', - }, + url: '/fixtures/kitten-4-112-112.jpg', + screenshotPath: '/fixtures/kitten-4-112-112.jpg', }), - fakeAttachment({ + fakeDraftAttachment({ contentType: IMAGE_GIF, fileName: 'giphy-GVNv0UpeYm17e', url: '/fixtures/giphy-GVNvOUpeYmI7e.gif', diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index 1d767972cb..f8144ec748 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -7,21 +7,20 @@ import { Image } from './Image'; import { StagedGenericAttachment } from './StagedGenericAttachment'; import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; import type { LocalizerType } from '../../types/Util'; -import type { AttachmentType } from '../../types/Attachment'; +import type { AttachmentDraftType } from '../../types/Attachment'; import { areAllAttachmentsVisual, - getUrl, isImageAttachment, isVideoAttachment, } from '../../types/Attachment'; export type Props = Readonly<{ - attachments: ReadonlyArray; + attachments: ReadonlyArray; i18n: LocalizerType; onAddAttachment?: () => void; - onClickAttachment?: (attachment: AttachmentType) => void; + onClickAttachment?: (attachment: AttachmentDraftType) => void; onClose?: () => void; - onCloseAttachment: (attachment: AttachmentType) => void; + onCloseAttachment: (attachment: AttachmentDraftType) => void; }>; const IMAGE_WIDTH = 120; @@ -31,6 +30,14 @@ const IMAGE_HEIGHT = 120; const BLANK_VIDEO_THUMBNAIL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR42mNiAAAABgADm78GJQAAAABJRU5ErkJggg=='; +function getUrl(attachment: AttachmentDraftType): string | undefined { + if (attachment.pending) { + return undefined; + } + + return attachment.url; +} + export const AttachmentList = ({ attachments, i18n, @@ -65,11 +72,17 @@ export const AttachmentList = ({ const isImage = isImageAttachment(attachment); const isVideo = isVideoAttachment(attachment); + const closeAttachment = () => onCloseAttachment(attachment); if (isImage || isVideo || attachment.pending) { + const isDownloaded = !attachment.pending; const imageUrl = url || (isVideo ? BLANK_VIDEO_THUMBNAIL : undefined); + const clickAttachment = onClickAttachment + ? () => onClickAttachment(attachment) + : undefined; + return ( { - onCloseAttachment(attachment); - }} + onClick={clickAttachment} + onClickClose={closeAttachment} + onError={closeAttachment} /> ); } @@ -99,7 +111,7 @@ export const AttachmentList = ({ key={key} attachment={attachment} i18n={i18n} - onClose={onCloseAttachment} + onClose={closeAttachment} /> ); })} diff --git a/ts/components/conversation/AudioCapture.tsx b/ts/components/conversation/AudioCapture.tsx index fc93b42eef..ea1ef03460 100644 --- a/ts/components/conversation/AudioCapture.tsx +++ b/ts/components/conversation/AudioCapture.tsx @@ -5,7 +5,10 @@ import React, { useCallback, useEffect, useState } from 'react'; import * as moment from 'moment'; import { noop } from 'lodash'; -import type { AttachmentType } from '../../types/Attachment'; +import type { + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../../types/Attachment'; import { ConfirmationDialog } from '../ConfirmationDialog'; import type { LocalizerType } from '../../types/Util'; import { @@ -20,7 +23,7 @@ import { useKeyboardShortcuts, } from '../../hooks/useKeyboardShortcuts'; -type OnSendAudioRecordingType = (rec: AttachmentType) => unknown; +type OnSendAudioRecordingType = (rec: InMemoryAttachmentDraftType) => unknown; export type PropsType = { cancelRecording: () => unknown; @@ -29,7 +32,7 @@ export type PropsType = { conversationId: string, onSendAudioRecording?: OnSendAudioRecordingType ) => unknown; - draftAttachments: ReadonlyArray; + draftAttachments: ReadonlyArray; errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType; errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; i18n: LocalizerType; diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index b9fa523cef..dcdbdc5388 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -15,6 +15,7 @@ export type Props = { attachment: AttachmentType; url?: string; + isDownloaded?: boolean; className?: string; height?: number; width?: number; @@ -145,6 +146,7 @@ export class Image extends React.Component { curveTopLeft, curveTopRight, darkOverlay, + isDownloaded, height = 0, i18n, noBackground, @@ -165,7 +167,9 @@ export class Image extends React.Component { const { caption, pending } = attachment || { caption: null, pending: true }; const canClick = this.canClick(); - const imgNotDownloaded = hasNotDownloaded(attachment); + const imgNotDownloaded = isDownloaded + ? false + : hasNotDownloaded(attachment); const resolvedBlurHash = blurHash || defaultBlurHash(theme); diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index b67c89419c..0eca5000e6 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -22,7 +22,11 @@ import { } from './messages/MessageSendState'; import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions'; import { ConversationColorType } from './types/Colors'; -import { AttachmentType, ThumbnailType } from './types/Attachment'; +import { + AttachmentDraftType, + AttachmentType, + ThumbnailType, +} from './types/Attachment'; import { EmbeddedContactType } from './types/EmbeddedContact'; import { SignalService as Proto } from './protobuf'; import { AvatarDataType } from './types/Avatar'; @@ -223,7 +227,7 @@ export type ConversationAttributesType = { customColorId?: string; discoveredUnregisteredAt?: number; draftChanged?: boolean; - draftAttachments?: Array; + draftAttachments?: Array; draftBodyRanges?: Array; draftTimestamp?: number | null; inbox_position: number; diff --git a/ts/state/ducks/audioRecorder.ts b/ts/state/ducks/audioRecorder.ts index fe91e7a247..022aa4f4b8 100644 --- a/ts/state/ducks/audioRecorder.ts +++ b/ts/state/ducks/audioRecorder.ts @@ -4,7 +4,7 @@ import type { ThunkAction } from 'redux-thunk'; import * as log from '../../logging/log'; -import type { AttachmentType } from '../../types/Attachment'; +import type { InMemoryAttachmentDraftType } from '../../types/Attachment'; import { SignalService as Proto } from '../../protobuf'; import type { StateType as RootStateType } from '../reducer'; import { fileToBytes } from '../../util/fileToBytes'; @@ -129,7 +129,7 @@ function completeRecordingAction(): CompleteRecordingAction { function completeRecording( conversationId: string, - onSendAudioRecording?: (rec: AttachmentType) => unknown + onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown ): ThunkAction< void, RootStateType, @@ -158,7 +158,8 @@ function completeRecording( } const data = await fileToBytes(blob); - const voiceNoteAttachment = { + const voiceNoteAttachment: InMemoryAttachmentDraftType = { + pending: false, contentType: stringToMIMEType(blob.type), data, size: data.byteLength, diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index e1d5498c0a..cecab51e00 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -6,7 +6,10 @@ import type { ThunkAction } from 'redux-thunk'; import * as log from '../../logging/log'; import type { NoopActionType } from './noop'; import type { StateType as RootStateType } from '../reducer'; -import type { AttachmentType } from '../../types/Attachment'; +import type { + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../../types/Attachment'; import type { MessageAttributesType } from '../../model-types.d'; import type { LinkPreviewWithDomain } from '../../types/LinkPreview'; import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; @@ -15,14 +18,14 @@ import { REMOVE_PREVIEW as REMOVE_LINK_PREVIEW } from './linkPreviews'; import { writeDraftAttachment } from '../../util/writeDraftAttachment'; import { deleteDraftAttachment } from '../../util/deleteDraftAttachment'; import { replaceIndex } from '../../util/replaceIndex'; -import { resolveAttachmentOnDisk } from '../../util/resolveAttachmentOnDisk'; +import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; import type { HandleAttachmentsProcessingArgsType } from '../../util/handleAttachmentsProcessing'; import { handleAttachmentsProcessing } from '../../util/handleAttachmentsProcessing'; // State export type ComposerStateType = { - attachments: ReadonlyArray; + attachments: ReadonlyArray; linkPreviewLoading: boolean; linkPreviewResult?: LinkPreviewWithDomain; quotedMessage?: Pick; @@ -40,12 +43,12 @@ const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE'; type AddPendingAttachmentActionType = { type: typeof ADD_PENDING_ATTACHMENT; - payload: AttachmentType; + payload: AttachmentDraftType; }; type ReplaceAttachmentsActionType = { type: typeof REPLACE_ATTACHMENTS; - payload: ReadonlyArray; + payload: ReadonlyArray; }; type ResetComposerActionType = { @@ -99,14 +102,14 @@ export const actions = { // next in-memory store. function getAttachmentsFromConversationModel( conversationId: string -): Array { +): Array { const conversation = window.ConversationController.get(conversationId); return conversation?.get('draftAttachments') || []; } function addAttachment( conversationId: string, - attachment: AttachmentType + attachment: InMemoryAttachmentDraftType ): ThunkAction { return async (dispatch, getState) => { // We do async operations first so multiple in-process addAttachments don't stomp on @@ -161,7 +164,7 @@ function addAttachment( function addPendingAttachment( conversationId: string, - pendingAttachment: AttachmentType + pendingAttachment: AttachmentDraftType ): ThunkAction { return (dispatch, getState) => { const isSelectedConversation = @@ -240,7 +243,7 @@ function removeAttachment( function replaceAttachments( conversationId: string, - attachments: ReadonlyArray + attachments: ReadonlyArray ): ThunkAction { return (dispatch, getState) => { // If the call came from a conversation we are no longer in we do not @@ -251,7 +254,7 @@ function replaceAttachments( dispatch({ type: REPLACE_ATTACHMENTS, - payload: attachments.map(resolveAttachmentOnDisk), + payload: attachments.map(resolveDraftAttachmentOnDisk), }); }; } diff --git a/ts/state/smart/ForwardMessageModal.tsx b/ts/state/smart/ForwardMessageModal.tsx index cf9b845671..8db775918c 100644 --- a/ts/state/smart/ForwardMessageModal.tsx +++ b/ts/state/smart/ForwardMessageModal.tsx @@ -13,15 +13,15 @@ import { getLinkPreview } from '../selectors/linkPreviews'; import { getIntl, getTheme } from '../selectors/user'; import { getEmojiSkinTone } from '../selectors/items'; import { selectRecentEmojis } from '../selectors/emojis'; -import type { AttachmentType } from '../../types/Attachment'; +import type { AttachmentDraftType } from '../../types/Attachment'; export type SmartForwardMessageModalProps = { - attachments?: Array; + attachments?: Array; conversationId: string; doForwardMessage: ( selectedContacts: Array, messageBody?: string, - attachments?: Array, + attachments?: Array, linkPreview?: LinkPreviewType ) => void; isSticker: boolean; diff --git a/ts/test-both/helpers/fakeAttachment.ts b/ts/test-both/helpers/fakeAttachment.ts index 2b300ecaa5..d4c2a22e00 100644 --- a/ts/test-both/helpers/fakeAttachment.ts +++ b/ts/test-both/helpers/fakeAttachment.ts @@ -1,7 +1,10 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { AttachmentType } from '../../types/Attachment'; +import type { + AttachmentType, + AttachmentDraftType, +} from '../../types/Attachment'; import { IMAGE_JPEG } from '../../types/MIME'; export const fakeAttachment = ( @@ -13,3 +16,13 @@ export const fakeAttachment = ( size: 10304, ...overrides, }); + +export const fakeDraftAttachment = ( + overrides: Partial = {} +): AttachmentDraftType => ({ + pending: false, + contentType: IMAGE_JPEG, + path: 'file.jpg', + size: 10304, + ...overrides, +}); diff --git a/ts/test-both/state/ducks/composer_test.ts b/ts/test-both/state/ducks/composer_test.ts index f7b751dbfc..1caaaf3f6c 100644 --- a/ts/test-both/state/ducks/composer_test.ts +++ b/ts/test-both/state/ducks/composer_test.ts @@ -9,8 +9,8 @@ import { noopAction } from '../../../state/ducks/noop'; import { reducer as rootReducer } from '../../../state/reducer'; import { IMAGE_JPEG } from '../../../types/MIME'; -import type { AttachmentType } from '../../../types/Attachment'; -import { fakeAttachment } from '../../helpers/fakeAttachment'; +import type { AttachmentDraftType } from '../../../types/Attachment'; +import { fakeDraftAttachment } from '../../helpers/fakeAttachment'; describe('both/state/ducks/composer', () => { const QUOTED_MESSAGE = { @@ -40,8 +40,13 @@ describe('both/state/ducks/composer', () => { const { replaceAttachments } = actions; const dispatch = sinon.spy(); - const attachments: Array = [ - { contentType: IMAGE_JPEG, pending: false, url: '', size: 2433 }, + const attachments: Array = [ + { + contentType: IMAGE_JPEG, + pending: true, + size: 2433, + path: 'image.jpg', + }, ]; replaceAttachments('123', attachments)( dispatch, @@ -57,7 +62,7 @@ describe('both/state/ducks/composer', () => { it('sets the high quality setting to false when there are no attachments', () => { const { replaceAttachments } = actions; const dispatch = sinon.spy(); - const attachments: Array = []; + const attachments: Array = []; replaceAttachments('123', attachments)( dispatch, @@ -83,7 +88,7 @@ describe('both/state/ducks/composer', () => { const { replaceAttachments } = actions; const dispatch = sinon.spy(); - const attachments = [fakeAttachment()]; + const attachments = [fakeDraftAttachment()]; replaceAttachments('123', attachments)( dispatch, getRootStateFunction('456'), diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index f1f73738a2..b45aa7791f 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -78,40 +78,46 @@ export type DownloadedAttachmentType = AttachmentType & { export type BaseAttachmentDraftType = { blurHash?: string; contentType: MIME.MIMEType; - fileName: string; - path: string; screenshotContentType?: string; screenshotSize?: number; size: number; + flags?: number; }; +// An ephemeral attachment type, used between user's request to add the attachment as +// a draft and final save on disk and in conversation.draftAttachments. export type InMemoryAttachmentDraftType = | ({ - data?: Uint8Array; + data: Uint8Array; pending: false; screenshotData?: Uint8Array; + fileName?: string; + path?: string; } & BaseAttachmentDraftType) | { contentType: MIME.MIMEType; - fileName: string; - path: string; + fileName?: string; + path?: string; pending: true; size: number; }; +// What's stored in conversation.draftAttachments export type AttachmentDraftType = | ({ - url: string; + url?: string; screenshotPath?: string; pending: false; // Old draft attachments may have a caption, though they are no longer editable // because we removed the caption editor. caption?: string; + fileName?: string; + path: string; } & BaseAttachmentDraftType) | { contentType: MIME.MIMEType; - fileName: string; - path: string; + fileName?: string; + path?: string; pending: true; size: number; }; @@ -614,6 +620,10 @@ export function getUrl(attachment: AttachmentType): string | undefined { return attachment.screenshot.url; } + if (isVideoAttachment(attachment)) { + return undefined; + } + return attachment.url; } diff --git a/ts/util/handleAttachmentsProcessing.ts b/ts/util/handleAttachmentsProcessing.ts index 9653846715..3ca2b3f210 100644 --- a/ts/util/handleAttachmentsProcessing.ts +++ b/ts/util/handleAttachmentsProcessing.ts @@ -6,17 +6,20 @@ import { preProcessAttachment, processAttachment, } from './processAttachment'; -import type { AttachmentType } from '../types/Attachment'; +import type { + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../types/Attachment'; import { AttachmentToastType } from '../types/AttachmentToastType'; import * as log from '../logging/log'; export type AddAttachmentActionType = ( conversationId: string, - attachment: AttachmentType + attachment: InMemoryAttachmentDraftType ) => unknown; export type AddPendingAttachmentActionType = ( conversationId: string, - pendingAttachment: AttachmentType + pendingAttachment: AttachmentDraftType ) => unknown; export type RemoveAttachmentActionType = ( conversationId: string, @@ -27,7 +30,7 @@ export type HandleAttachmentsProcessingArgsType = { addAttachment: AddAttachmentActionType; addPendingAttachment: AddPendingAttachmentActionType; conversationId: string; - draftAttachments: ReadonlyArray; + draftAttachments: ReadonlyArray; files: ReadonlyArray; onShowToast: (toastType: AttachmentToastType) => unknown; removeAttachment: RemoveAttachmentActionType; diff --git a/ts/util/processAttachment.ts b/ts/util/processAttachment.ts index 01264a40a1..d818dfb88d 100644 --- a/ts/util/processAttachment.ts +++ b/ts/util/processAttachment.ts @@ -4,7 +4,10 @@ import path from 'path'; import * as log from '../logging/log'; -import type { AttachmentType } from '../types/Attachment'; +import type { + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../types/Attachment'; import { AttachmentToastType } from '../types/AttachmentToastType'; import { fileToBytes } from './fileToBytes'; import { handleImageAttachment } from './handleImageAttachment'; @@ -14,7 +17,9 @@ import { isFileDangerous } from './isFileDangerous'; import { isHeic, isImage, stringToMIMEType } from '../types/MIME'; import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome'; -export function getPendingAttachment(file: File): AttachmentType | undefined { +export function getPendingAttachment( + file: File +): AttachmentDraftType | undefined { if (!file) { return; } @@ -33,7 +38,7 @@ export function getPendingAttachment(file: File): AttachmentType | undefined { export function preProcessAttachment( file: File, - draftAttachments: Array + draftAttachments: Array ): AttachmentToastType | undefined { if (!file) { return; @@ -53,7 +58,7 @@ export function preProcessAttachment( } const haveNonImage = draftAttachments.some( - (attachment: AttachmentType) => !isImage(attachment.contentType) + (attachment: AttachmentDraftType) => !isImage(attachment.contentType) ); // You can't add another attachment if you already have a non-image staged if (haveNonImage) { @@ -72,10 +77,10 @@ export function preProcessAttachment( export async function processAttachment( file: File -): Promise { +): Promise { const fileType = stringToMIMEType(file.type); - let attachment: AttachmentType; + let attachment: InMemoryAttachmentDraftType; try { if (isImageTypeSupported(fileType) || isHeic(fileType)) { attachment = await handleImageAttachment(file); diff --git a/ts/util/resolveAttachmentOnDisk.ts b/ts/util/resolveDraftAttachmentOnDisk.ts similarity index 71% rename from ts/util/resolveAttachmentOnDisk.ts rename to ts/util/resolveDraftAttachmentOnDisk.ts index ff663a3c89..27765aaa68 100644 --- a/ts/util/resolveAttachmentOnDisk.ts +++ b/ts/util/resolveDraftAttachmentOnDisk.ts @@ -4,11 +4,12 @@ import { pick } from 'lodash'; import * as log from '../logging/log'; -import type { AttachmentType } from '../types/Attachment'; +import type { AttachmentDraftType } from '../types/Attachment'; +import { isVideoAttachment } from '../types/Attachment'; -export function resolveAttachmentOnDisk( - attachment: AttachmentType -): AttachmentType { +export function resolveDraftAttachmentOnDisk( + attachment: AttachmentDraftType +): AttachmentDraftType { let url = ''; if (attachment.pending) { return attachment; @@ -18,7 +19,7 @@ export function resolveAttachmentOnDisk( url = window.Signal.Migrations.getAbsoluteDraftPath( attachment.screenshotPath ); - } else if (attachment.path) { + } else if (!isVideoAttachment(attachment) && attachment.path) { url = window.Signal.Migrations.getAbsoluteDraftPath(attachment.path); } else { log.warn( diff --git a/ts/util/writeDraftAttachment.ts b/ts/util/writeDraftAttachment.ts index 9603df563b..b233ebd920 100644 --- a/ts/util/writeDraftAttachment.ts +++ b/ts/util/writeDraftAttachment.ts @@ -2,28 +2,32 @@ // SPDX-License-Identifier: AGPL-3.0-only import { omit } from 'lodash'; -import type { AttachmentType } from '../types/Attachment'; +import type { + InMemoryAttachmentDraftType, + AttachmentDraftType, +} from '../types/Attachment'; export async function writeDraftAttachment( - attachment: AttachmentType -): Promise { + attachment: InMemoryAttachmentDraftType +): Promise { if (attachment.pending) { throw new Error('writeDraftAttachment: Cannot write pending attachment'); } - const result: AttachmentType = { + const path = await window.Signal.Migrations.writeNewDraftData( + attachment.data + ); + + const screenshotPath = attachment.screenshotData + ? await window.Signal.Migrations.writeNewDraftData( + attachment.screenshotData + ) + : undefined; + + return { ...omit(attachment, ['data', 'screenshotData']), + path, + screenshotPath, pending: false, }; - if (attachment.data) { - result.path = await window.Signal.Migrations.writeNewDraftData( - attachment.data - ); - } - if (attachment.screenshotData) { - result.screenshotPath = await window.Signal.Migrations.writeNewDraftData( - attachment.screenshotData - ); - } - return result; } diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 4ff9c79cee..9929a1a69e 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -7,7 +7,7 @@ import { batch as batchDispatch } from 'react-redux'; import { debounce, flatten, omit, throttle } from 'lodash'; import { render } from 'mustache'; -import type { AttachmentType } from '../types/Attachment'; +import type { AttachmentDraftType, AttachmentType } from '../types/Attachment'; import { isGIF } from '../types/Attachment'; import * as Attachment from '../types/Attachment'; import type { StickerPackType as StickerPackDBType } from '../sql/Interface'; @@ -1561,16 +1561,32 @@ export class ConversationView extends window.Backbone.View { } const attachments = getAttachmentsForMessage(message.attributes); + const draftAttachments = attachments + .map((item: AttachmentType): AttachmentDraftType | null => { + const { path } = item; + if (!path) { + return null; + } + + return { + ...item, + path, + pending: false as const, + screenshotPath: item.screenshot?.path, + }; + }) + .filter(isNotNil); + this.forwardMessageModal = new Whisper.ReactWrapperView({ JSX: window.Signal.State.Roots.createForwardMessageModal( window.reduxStore, { - attachments, + attachments: draftAttachments, conversationId: this.model.id, doForwardMessage: async ( conversationIds: Array, messageBody?: string, - includedAttachments?: Array, + includedAttachments?: Array, linkPreview?: LinkPreviewType ) => { try { @@ -1619,7 +1635,7 @@ export class ConversationView extends window.Backbone.View { message: MessageModel, conversationIds: Array, messageBody?: string, - attachments?: Array, + attachments?: Array, linkPreview?: LinkPreviewType ): Promise { log.info(`maybeForwardMessage/${message.idForLogging()}: Starting...`);