From b138774454acf84662bb49619e6561cdcb526803 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:12:04 -0500 Subject: [PATCH] Moves saveAttachment to a redux action --- ts/background.ts | 5 +- ts/components/App.tsx | 9 ++- ts/components/AvatarLightbox.tsx | 3 +- ts/components/Lightbox.stories.tsx | 1 + ts/components/Lightbox.tsx | 8 +- ts/components/StoryDetailsModal.tsx | 4 +- ts/components/StoryViewer.tsx | 8 +- ts/components/StoryViewsNRepliesModal.tsx | 1 + .../ToastDangerousFileType.stories.tsx | 28 ------- ts/components/ToastDangerousFileType.tsx | 18 ----- ts/components/ToastFileSaved.stories.tsx | 29 ------- ts/components/ToastFileSaved.tsx | 33 -------- ts/components/ToastManager.stories.tsx | 10 +++ ts/components/ToastManager.tsx | 20 +++++ ts/components/conversation/Message.tsx | 12 ++- .../conversation/MessageDetail.stories.tsx | 1 + ts/components/conversation/MessageDetail.tsx | 3 + ts/components/conversation/Quote.stories.tsx | 1 + .../conversation/Timeline.stories.tsx | 1 + ts/components/conversation/Timeline.tsx | 1 + .../conversation/TimelineItem.stories.tsx | 1 + .../conversation/TimelineMessage.stories.tsx | 1 + .../conversation/TimelineMessage.tsx | 3 +- ts/signal.ts | 4 - ts/state/ducks/conversations.ts | 81 +++++++++++++++++++ ts/state/ducks/lightbox.ts | 10 ++- ts/state/ducks/toast.ts | 15 +++- ts/state/smart/Lightbox.tsx | 3 + ts/state/smart/Stories.tsx | 4 +- ts/state/smart/StoryViewer.tsx | 4 +- ts/util/saveAttachment.ts | 66 --------------- ts/util/showToast.tsx | 8 -- ts/views/conversation_view.tsx | 6 +- ts/windows/attachments.ts | 4 - 34 files changed, 190 insertions(+), 216 deletions(-) delete mode 100644 ts/components/ToastDangerousFileType.stories.tsx delete mode 100644 ts/components/ToastDangerousFileType.tsx delete mode 100644 ts/components/ToastFileSaved.stories.tsx delete mode 100644 ts/components/ToastFileSaved.tsx delete mode 100644 ts/util/saveAttachment.ts diff --git a/ts/background.ts b/ts/background.ts index 1ef22ed5a..813a140ad 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -158,7 +158,6 @@ import type AccountManager from './textsecure/AccountManager'; import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate'; import { StoryViewModeType, StoryViewTargetType } from './types/Stories'; import { downloadOnboardingStory } from './util/downloadOnboardingStory'; -import { saveAttachmentFromMessage } from './util/saveAttachment'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -1661,7 +1660,9 @@ export async function startApp(): Promise { event.preventDefault(); event.stopPropagation(); - saveAttachmentFromMessage(selectedMessage); + window.reduxActions.conversations.saveAttachmentFromMessage( + selectedMessage + ); return; } } diff --git a/ts/components/App.tsx b/ts/components/App.tsx index 4ffcdace8..16575d8db 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -41,6 +41,7 @@ type PropsType = { isMaximized: boolean; isFullScreen: boolean; menuOptions: MenuOptionsType; + openFileInFolder: (target: string) => unknown; hasCustomTitleBar: boolean; hideMenuBar: boolean; @@ -73,6 +74,7 @@ export function App({ hasCustomTitleBar, menuOptions, openInbox, + openFileInFolder, registerSingleDevice, renderCallManager, renderCustomizingPreferredReactionsModal, @@ -178,7 +180,12 @@ export function App({ 'dark-theme': theme === ThemeType.dark, })} > - + {renderGlobalModalContainer()} {renderCallManager()} {renderLightbox()} diff --git a/ts/components/AvatarLightbox.tsx b/ts/components/AvatarLightbox.tsx index 70926092f..c8fd1af82 100644 --- a/ts/components/AvatarLightbox.tsx +++ b/ts/components/AvatarLightbox.tsx @@ -30,8 +30,9 @@ export function AvatarLightbox({ = {}): PropsType => ({ i18n, isViewOnce: Boolean(overrideProps.isViewOnce), media: overrideProps.media || [], + saveAttachment: action('saveAttachment'), selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), toggleForwardMessageModal: action('toggleForwardMessageModal'), }); diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 3c037421a..efe6f6d02 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -9,7 +9,10 @@ import { createPortal } from 'react-dom'; import { noop } from 'lodash'; import { useSpring, animated, to } from '@react-spring/web'; -import type { ConversationType } from '../state/ducks/conversations'; +import type { + ConversationType, + SaveAttachmentActionCreatorType, +} from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import * as GoogleChrome from '../util/GoogleChrome'; @@ -18,7 +21,6 @@ import { Avatar, AvatarSize } from './Avatar'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import { formatDuration } from '../util/formatDuration'; import { isGIF } from '../types/Attachment'; -import { saveAttachment } from '../util/saveAttachment'; import { useRestoreFocus } from '../hooks/useRestoreFocus'; export type PropsType = { @@ -28,6 +30,7 @@ export type PropsType = { i18n: LocalizerType; isViewOnce?: boolean; media: Array; + saveAttachment: SaveAttachmentActionCreatorType; selectedIndex?: number; toggleForwardMessageModal: (messageId: string) => unknown; }; @@ -53,6 +56,7 @@ export function Lightbox({ media, i18n, isViewOnce = false, + saveAttachment, selectedIndex: initialSelectedIndex = 0, toggleForwardMessageModal, }: PropsType): JSX.Element | null { diff --git a/ts/components/StoryDetailsModal.tsx b/ts/components/StoryDetailsModal.tsx index 7af1945c8..3b7e49a9e 100644 --- a/ts/components/StoryDetailsModal.tsx +++ b/ts/components/StoryDetailsModal.tsx @@ -15,7 +15,7 @@ import { SendStatus } from '../messages/MessageSendState'; import { Theme } from '../util/theme'; import { formatDateTimeLong } from '../util/timestamp'; import { DurationInSeconds } from '../util/durations'; -import type { saveAttachment } from '../util/saveAttachment'; +import type { SaveAttachmentActionCreatorType } from '../state/ducks/conversations'; import type { AttachmentType } from '../types/Attachment'; import { ThemeType } from '../types/Util'; import { Time } from './Time'; @@ -27,7 +27,7 @@ export type PropsType = { i18n: LocalizerType; isInternalUser?: boolean; onClose: () => unknown; - saveAttachment: typeof saveAttachment; + saveAttachment: SaveAttachmentActionCreatorType; sender: StoryViewType['sender']; sendState?: Array; attachment?: AttachmentType; diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index b7322abdc..f9ccad5b9 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -12,7 +12,10 @@ import React, { import classNames from 'classnames'; import type { DraftBodyRangesType, LocalizerType } from '../types/Util'; import type { ContextMenuOptionType } from './ContextMenu'; -import type { ConversationType } from '../state/ducks/conversations'; +import type { + ConversationType, + SaveAttachmentActionCreatorType, +} from '../state/ducks/conversations'; import type { EmojiPickDataType } from './emoji/EmojiPicker'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; @@ -44,7 +47,6 @@ import { ToastType } from '../state/ducks/toast'; import { getAvatarColor } from '../types/Colors'; import { getStoryBackground } from '../util/getStoryBackground'; import { getStoryDuration } from '../util/getStoryDuration'; -import type { saveAttachment } from '../util/saveAttachment'; import { isVideoAttachment } from '../types/Attachment'; import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; @@ -100,7 +102,7 @@ export type PropsType = { renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; replyState?: ReplyStateType; retrySend: (messageId: string) => unknown; - saveAttachment: typeof saveAttachment; + saveAttachment: SaveAttachmentActionCreatorType; setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown; showToast: ShowToastActionCreatorType; skinTone?: number; diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index 23b1153e4..2032d0bc8 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -62,6 +62,7 @@ const MESSAGE_DEFAULT_PROPS = { openLink: shouldNeverBeCalled, previews: [], renderAudioAttachment: () =>
, + saveAttachment: shouldNeverBeCalled, scrollToQuotedMessage: shouldNeverBeCalled, showContactDetail: shouldNeverBeCalled, showContactModal: shouldNeverBeCalled, diff --git a/ts/components/ToastDangerousFileType.stories.tsx b/ts/components/ToastDangerousFileType.stories.tsx deleted file mode 100644 index 283b7077d..000000000 --- a/ts/components/ToastDangerousFileType.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastDangerousFileType } from './ToastDangerousFileType'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastDangerousFileType', -}; - -export const _ToastDangerousFileType = (): JSX.Element => ( - -); - -_ToastDangerousFileType.story = { - name: 'ToastDangerousFileType', -}; diff --git a/ts/components/ToastDangerousFileType.tsx b/ts/components/ToastDangerousFileType.tsx deleted file mode 100644 index 556cd39ab..000000000 --- a/ts/components/ToastDangerousFileType.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastDangerousFileType({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('dangerousFileType')}; -} diff --git a/ts/components/ToastFileSaved.stories.tsx b/ts/components/ToastFileSaved.stories.tsx deleted file mode 100644 index bb13e533b..000000000 --- a/ts/components/ToastFileSaved.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastFileSaved } from './ToastFileSaved'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), - onOpenFile: action('onOpenFile'), -}; - -export default { - title: 'Components/ToastFileSaved', -}; - -export const _ToastFileSaved = (): JSX.Element => ( - -); - -_ToastFileSaved.story = { - name: 'ToastFileSaved', -}; diff --git a/ts/components/ToastFileSaved.tsx b/ts/components/ToastFileSaved.tsx deleted file mode 100644 index 2658c8679..000000000 --- a/ts/components/ToastFileSaved.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -export type ToastPropsType = { - onOpenFile: () => unknown; -}; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -} & ToastPropsType; - -export function ToastFileSaved({ - i18n, - onClose, - onOpenFile, -}: PropsType): JSX.Element { - return ( - - {i18n('attachmentSaved')} - - ); -} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 08dfc8ef6..95bd758ec 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -126,6 +126,16 @@ FailedToDeleteUsername.args = { }, }; +export const FileSaved = Template.bind({}); +FileSaved.args = { + toast: { + toastType: ToastType.FileSaved, + parameters: { + fullPath: '/image.png', + }, + }, +}; + export const FileSize = Template.bind({}); FileSize.args = { toast: { diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index af0027392..62075b010 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -12,6 +12,7 @@ import { missingCaseError } from '../util/missingCaseError'; export type PropsType = { hideToast: () => unknown; i18n: LocalizerType; + openFileInFolder: (target: string) => unknown; toast?: { toastType: ToastType; parameters?: ReplacementValuesType; @@ -23,6 +24,7 @@ const SHORT_TIMEOUT = 3 * SECOND; export function ToastManager({ hideToast, i18n, + openFileInFolder, toast, }: PropsType): JSX.Element | null { if (toast === undefined) { @@ -117,6 +119,24 @@ export function ToastManager({ ); } + if (toastType === ToastType.FileSaved) { + return ( + { + if (toast.parameters && 'fullPath' in toast.parameters) { + openFileInFolder(String(toast.parameters.fullPath)); + } + }, + }} + > + {i18n('attachmentSaved')} + + ); + } + if (toastType === ToastType.FileSize) { return ( diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 321b614e5..2dc19f6df 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -14,6 +14,7 @@ import type { ConversationType, ConversationTypeType, InteractionModeType, + SaveAttachmentActionCreatorType, } from '../../state/ducks/conversations'; import type { ViewStoryActionCreatorType } from '../../state/ducks/stories'; import type { ReadStatus } from '../../messages/MessageReadStatus'; @@ -87,7 +88,6 @@ import { PaymentEventKind } from '../../types/Payment'; import type { AnyPaymentEvent } from '../../types/Payment'; import { Emojify } from './Emojify'; import { getPaymentEventDescription } from '../../messages/helpers'; -import { saveAttachment } from '../../util/saveAttachment'; const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10; const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18; @@ -319,6 +319,7 @@ export type PropsActions = { messageId: string; }) => void; markViewed(messageId: string): void; + saveAttachment: SaveAttachmentActionCreatorType; showLightbox: (options: { attachment: AttachmentType; messageId: string; @@ -2380,8 +2381,13 @@ export class Message extends React.PureComponent { }; public openGenericAttachment = (event?: React.MouseEvent): void => { - const { id, attachments, timestamp, kickOffAttachmentDownload } = - this.props; + const { + id, + attachments, + saveAttachment, + timestamp, + kickOffAttachmentDownload, + } = this.props; if (event) { event.preventDefault(); diff --git a/ts/components/conversation/MessageDetail.stories.tsx b/ts/components/conversation/MessageDetail.stories.tsx index 3973abf9a..b20a1f4d5 100644 --- a/ts/components/conversation/MessageDetail.stories.tsx +++ b/ts/components/conversation/MessageDetail.stories.tsx @@ -82,6 +82,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ openGiftBadge: action('openGiftBadge'), openLink: action('openLink'), renderAudioAttachment: () =>
*AudioAttachment*
, + saveAttachment: action('saveAttachment'), showContactDetail: action('showContactDetail'), showContactModal: action('showContactModal'), showExpiredIncomingTapToViewToast: action( diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 675bb6426..0c3fdf451 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -96,6 +96,7 @@ export type PropsReduxActions = Pick< | 'checkForAccount' | 'clearSelectedMessage' | 'doubleCheckMissingQuoteReference' + | 'saveAttachment' | 'showContactModal' | 'showLightbox' | 'showLightboxForViewOnceMedia' @@ -293,6 +294,7 @@ export class MessageDetail extends React.Component { openGiftBadge, openLink, renderAudioAttachment, + saveAttachment, showContactDetail, showContactModal, showExpiredIncomingTapToViewToast, @@ -338,6 +340,7 @@ export class MessageDetail extends React.Component { openGiftBadge={openGiftBadge} openLink={openLink} renderAudioAttachment={renderAudioAttachment} + saveAttachment={saveAttachment} shouldCollapseAbove={false} shouldCollapseBelow={false} shouldHideMetadata={false} diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 558660ec7..1de84a4bc 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -124,6 +124,7 @@ const defaultMessageProps: TimelineMessagesProps = { setQuoteByMessageId: action('default--setQuoteByMessageId'), retrySend: action('default--retrySend'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'), + saveAttachment: action('saveAttachment'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'), selectMessage: action('default--selectMessage'), shouldCollapseAbove: false, diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 5d3426024..afb187d7c 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -283,6 +283,7 @@ const actions = () => ({ deleteMessageForEveryone: action('deleteMessageForEveryone'), showMessageDetail: action('showMessageDetail'), openConversation: action('openConversation'), + saveAttachment: action('saveAttachment'), showContactDetail: action('showContactDetail'), showContactModal: action('showContactModal'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'), diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 275255c6b..64251dd14 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -253,6 +253,7 @@ const getActions = createSelector( 'kickOffAttachmentDownload', 'markAttachmentAsCorrupted', 'messageExpanded', + 'saveAttachment', 'showLightbox', 'showLightboxForViewOnceMedia', 'openLink', diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 67d295082..89a1be7fa 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -80,6 +80,7 @@ const getDefaultProps = () => ({ showMessageDetail: action('showMessageDetail'), openConversation: action('openConversation'), openGiftBadge: action('openGiftBadge'), + saveAttachment: action('saveAttachment'), showContactDetail: action('showContactDetail'), showContactModal: action('showContactModal'), showLightbox: action('showLightbox'), diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 24edf91b2..0fdf55aa6 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -293,6 +293,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ renderEmojiPicker, renderReactionPicker, renderAudioAttachment, + saveAttachment: action('saveAttachment'), setQuoteByMessageId: action('setQuoteByMessageId'), retrySend: action('retrySend'), retryDeleteForEveryone: action('retryDeleteForEveryone'), diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 533a1b45f..d6f04af43 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -27,7 +27,6 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore'; import type { Props as ReactionPickerProps } from './ReactionPicker'; import { ConfirmationDialog } from '../ConfirmationDialog'; import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; -import { saveAttachment } from '../../util/saveAttachment'; export type PropsData = { canDownload: boolean; @@ -172,7 +171,7 @@ export function TimelineMessage(props: Props): JSX.Element { }); const openGenericAttachment = (event?: React.MouseEvent): void => { - const { kickOffAttachmentDownload } = props; + const { kickOffAttachmentDownload, saveAttachment } = props; if (event) { event.preventDefault(); diff --git a/ts/signal.ts b/ts/signal.ts index 86f117865..c59ec2492 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -122,7 +122,6 @@ type MigrationsModuleType = { loadStickerData: ( sticker: StickerType | undefined ) => Promise; - openFileInFolder: (target: string) => Promise; readAttachmentData: (path: string) => Promise; readDraftData: (path: string) => Promise; readStickerData: (path: string) => Promise; @@ -185,7 +184,6 @@ export function initializeMigrations({ getStickersPath, getBadgesPath, getTempPath, - openFileInFolder, saveAttachmentToDisk, } = Attachments; const { @@ -266,7 +264,6 @@ export function initializeMigrations({ loadPreviewData, loadQuoteData, loadStickerData, - openFileInFolder, readAttachmentData, readDraftData, readStickerData, @@ -364,7 +361,6 @@ type AttachmentsModuleType = { ) => (relativePath: string) => string; createDoesExist: (root: string) => (relativePath: string) => Promise; - openFileInFolder: (target: string) => Promise; saveAttachmentToDisk: ({ data, name, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 7757cd62d..7864ea01a 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -21,6 +21,8 @@ import { getOwn } from '../../util/getOwn'; import { assertDev, strictAssert } from '../../util/assert'; import type { DurationInSeconds } from '../../util/durations'; import * as universalExpireTimer from '../../util/universalExpireTimer'; +import * as Attachment from '../../types/Attachment'; +import { isFileDangerous } from '../../util/isFileDangerous'; import type { ShowSendAnywayDialogActionType, ToggleProfileEditorErrorActionType, @@ -901,6 +903,8 @@ export const actions = { reviewGroupMemberNameCollision, reviewMessageRequestNameCollision, revokePendingMembershipsFromGroupV2, + saveAttachment, + saveAttachmentFromMessage, saveAvatarToDisk, scrollToMessage, selectMessage, @@ -2451,6 +2455,83 @@ function loadRecentMediaItems( }; } +export type SaveAttachmentActionCreatorType = ( + attachment: AttachmentType, + timestamp?: number, + index?: number +) => unknown; + +function saveAttachment( + attachment: AttachmentType, + timestamp = Date.now(), + index = 0 +): ThunkAction { + return async dispatch => { + const { fileName = '' } = attachment; + + const isDangerous = isFileDangerous(fileName); + + if (isDangerous) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType: ToastType.DangerousFileType, + }, + }); + return; + } + + const { readAttachmentData, saveAttachmentToDisk } = + window.Signal.Migrations; + + const fullPath = await Attachment.save({ + attachment, + index: index + 1, + readAttachmentData, + saveAttachmentToDisk, + timestamp, + }); + + if (fullPath) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType: ToastType.FileSaved, + parameters: { + fullPath, + }, + }, + }); + } + }; +} + +export function saveAttachmentFromMessage( + messageId: string, + providedAttachment?: AttachmentType +): ThunkAction { + return async (dispatch, getState) => { + const message = await getMessageById(messageId); + if (!message) { + throw new Error( + `saveAttachmentFromMessage: Message ${messageId} missing!` + ); + } + + const { attachments, sent_at: timestamp } = message.attributes; + if (!attachments || attachments.length < 1) { + return; + } + + const attachment = + providedAttachment && attachments.includes(providedAttachment) + ? providedAttachment + : attachments[0]; + + saveAttachment(attachment, timestamp)(dispatch, getState, null); + }; +} + function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType { return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' }; } diff --git a/ts/state/ducks/lightbox.ts b/ts/state/ducks/lightbox.ts index 5ce415afc..16e35cdd0 100644 --- a/ts/state/ducks/lightbox.ts +++ b/ts/state/ducks/lightbox.ts @@ -19,7 +19,7 @@ import { } from '../../util/GoogleChrome'; import { isTapToView } from '../selectors/message'; import { SHOW_TOAST, ToastType } from './toast'; -import { saveAttachmentFromMessage } from '../../util/saveAttachment'; +import { saveAttachmentFromMessage } from './conversations'; import { showStickerPackPreview } from './globalModals'; import { useBoundActions } from '../../hooks/useBoundActions'; @@ -213,7 +213,7 @@ function showLightbox(opts: { | ShowStickerPackPreviewActionType | ShowToastActionType > { - return async dispatch => { + return async (dispatch, getState) => { const { attachment, messageId } = opts; const message = await getMessageById(messageId); @@ -233,7 +233,11 @@ function showLightbox(opts: { !isImageTypeSupported(contentType) && !isVideoTypeSupported(contentType) ) { - await saveAttachmentFromMessage(messageId, attachment); + saveAttachmentFromMessage(messageId, attachment)( + dispatch, + getState, + null + ); return; } diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts index 87271207b..c5015c5b5 100644 --- a/ts/state/ducks/toast.ts +++ b/ts/state/ducks/toast.ts @@ -1,9 +1,12 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { ipcRenderer } from 'electron'; + import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; -import { useBoundActions } from '../../hooks/useBoundActions'; +import type { NoopActionType } from './noop'; import type { ReplacementValuesType } from '../../types/Util'; +import { useBoundActions } from '../../hooks/useBoundActions'; export enum ToastType { AddingUserToGroup = 'AddingUserToGroup', @@ -18,6 +21,7 @@ export enum ToastType { Error = 'Error', Expired = 'Expired', FailedToDeleteUsername = 'FailedToDeleteUsername', + FileSaved = 'FileSaved', FileSize = 'FileSize', InvalidConversation = 'InvalidConversation', LeftGroup = 'LeftGroup', @@ -72,6 +76,14 @@ function hideToast(): HideToastActionType { }; } +function openFileInFolder(target: string): NoopActionType { + ipcRenderer.send('show-item-in-folder', target); + return { + type: 'NOOP', + payload: null, + }; +} + export type ShowToastActionCreatorType = ( toastType: ToastType, parameters?: ReplacementValuesType @@ -92,6 +104,7 @@ export const showToast: ShowToastActionCreatorType = ( export const actions = { hideToast, + openFileInFolder, showToast, }; diff --git a/ts/state/smart/Lightbox.tsx b/ts/state/smart/Lightbox.tsx index 90ad34ab8..157a65b3c 100644 --- a/ts/state/smart/Lightbox.tsx +++ b/ts/state/smart/Lightbox.tsx @@ -11,6 +11,7 @@ import type { StateType } from '../reducer'; import { Lightbox } from '../../components/Lightbox'; import { getConversationSelector } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; +import { useConversationsActions } from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useLightboxActions } from '../ducks/lightbox'; import { @@ -22,6 +23,7 @@ import { export function SmartLightbox(): JSX.Element | null { const i18n = useSelector(getIntl); + const { saveAttachment } = useConversationsActions(); const { closeLightbox } = useLightboxActions(); const { toggleForwardMessageModal } = useGlobalModalActions(); @@ -45,6 +47,7 @@ export function SmartLightbox(): JSX.Element | null { i18n={i18n} isViewOnce={isViewOnce} media={media} + saveAttachment={saveAttachment} selectedIndex={selectedIndex || 0} toggleForwardMessageModal={toggleForwardMessageModal} /> diff --git a/ts/state/smart/Stories.tsx b/ts/state/smart/Stories.tsx index f26dd57b0..b82bc7d07 100644 --- a/ts/state/smart/Stories.tsx +++ b/ts/state/smart/Stories.tsx @@ -22,7 +22,6 @@ import { shouldShowStoriesView, } from '../selectors/stories'; import { retryMessageSend } from '../../util/retryMessageSend'; -import { saveAttachment } from '../../util/saveAttachment'; import { useConversationsActions } from '../ducks/conversations'; import { useGlobalModalActions } from '../ducks/globalModals'; import { useStoriesActions } from '../ducks/stories'; @@ -34,7 +33,8 @@ function renderStoryCreator(): JSX.Element { export function SmartStories(): JSX.Element | null { const storiesActions = useStoriesActions(); - const { showConversation, toggleHideStories } = useConversationsActions(); + const { saveAttachment, showConversation, toggleHideStories } = + useConversationsActions(); const { showStoriesSettings, toggleForwardMessageModal } = useGlobalModalActions(); const { showToast } = useToastActions(); diff --git a/ts/state/smart/StoryViewer.tsx b/ts/state/smart/StoryViewer.tsx index 263dd6669..f4bcc3210 100644 --- a/ts/state/smart/StoryViewer.tsx +++ b/ts/state/smart/StoryViewer.tsx @@ -29,7 +29,6 @@ import { isInFullScreenCall } from '../selectors/calling'; import { isSignalConversation } from '../../util/isSignalConversation'; import { renderEmojiPicker } from './renderEmojiPicker'; import { retryMessageSend } from '../../util/retryMessageSend'; -import { saveAttachment } from '../../util/saveAttachment'; import { strictAssert } from '../../util/assert'; import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled'; import { useActions as useEmojisActions } from '../ducks/emojis'; @@ -42,7 +41,8 @@ import { useIsWindowActive } from '../../hooks/useIsWindowActive'; export function SmartStoryViewer(): JSX.Element | null { const storiesActions = useStoriesActions(); const { onUseEmoji } = useEmojisActions(); - const { showConversation, toggleHideStories } = useConversationsActions(); + const { saveAttachment, showConversation, toggleHideStories } = + useConversationsActions(); const { onSetSkinTone } = useItemsActions(); const { showToast } = useToastActions(); diff --git a/ts/util/saveAttachment.ts b/ts/util/saveAttachment.ts deleted file mode 100644 index 6f545491f..000000000 --- a/ts/util/saveAttachment.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { AttachmentType } from '../types/Attachment'; -import * as Attachment from '../types/Attachment'; -import { ToastDangerousFileType } from '../components/ToastDangerousFileType'; -import { ToastFileSaved } from '../components/ToastFileSaved'; -import { isFileDangerous } from './isFileDangerous'; -import { showToast } from './showToast'; -import { getMessageById } from '../messages/getMessageById'; - -export async function saveAttachment( - attachment: AttachmentType, - timestamp = Date.now(), - index = 0 -): Promise { - const { fileName = '' } = attachment; - - const isDangerous = isFileDangerous(fileName); - - if (isDangerous) { - showToast(ToastDangerousFileType); - return; - } - - const { openFileInFolder, readAttachmentData, saveAttachmentToDisk } = - window.Signal.Migrations; - - const fullPath = await Attachment.save({ - attachment, - index: index + 1, - readAttachmentData, - saveAttachmentToDisk, - timestamp, - }); - - if (fullPath) { - showToast(ToastFileSaved, { - onOpenFile: () => { - openFileInFolder(fullPath); - }, - }); - } -} - -export async function saveAttachmentFromMessage( - messageId: string, - providedAttachment?: AttachmentType -): Promise { - const message = await getMessageById(messageId); - if (!message) { - throw new Error(`saveAttachmentFromMessage: Message ${messageId} missing!`); - } - - const { attachments, sent_at: timestamp } = message.attributes; - if (!attachments || attachments.length < 1) { - return; - } - - const attachment = - providedAttachment && attachments.includes(providedAttachment) - ? providedAttachment - : attachments[0]; - - return saveAttachment(attachment, timestamp); -} diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx index 30a1a9c8e..a18fe389c 100644 --- a/ts/util/showToast.tsx +++ b/ts/util/showToast.tsx @@ -22,10 +22,6 @@ import type { ToastInternalError, ToastPropsType as ToastInternalErrorPropsType, } from '../components/ToastInternalError'; -import type { - ToastFileSaved, - ToastPropsType as ToastFileSavedPropsType, -} from '../components/ToastFileSaved'; import type { ToastFileSize, ToastPropsType as ToastFileSizePropsType, @@ -61,10 +57,6 @@ export function showToast( Toast: typeof ToastInternalError, props: ToastInternalErrorPropsType ): void; -export function showToast( - Toast: typeof ToastFileSaved, - props: ToastFileSavedPropsType -): void; export function showToast( Toast: typeof ToastFileSize, props: ToastFileSizePropsType diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index cb64b6bbf..b824017d0 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -52,7 +52,6 @@ import { removeLinkPreview, suspendLinkPreviews, } from '../services/LinkPreview'; -import { saveAttachment } from '../util/saveAttachment'; import { SECOND } from '../util/durations'; import { startConversation } from '../util/startConversation'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; @@ -749,7 +748,10 @@ export class ConversationView extends window.Backbone.View { }: ItemClickEvent) => { switch (type) { case 'documents': { - saveAttachment(attachment, message.sent_at); + window.reduxActions.conversations.saveAttachment( + attachment, + message.sent_at + ); break; } diff --git a/ts/windows/attachments.ts b/ts/windows/attachments.ts index ab57b70bb..686dda450 100644 --- a/ts/windows/attachments.ts +++ b/ts/windows/attachments.ts @@ -197,10 +197,6 @@ export const createDoesExist = ( }; }; -export const openFileInFolder = async (target: string): Promise => { - ipcRenderer.send('show-item-in-folder', target); -}; - const showSaveDialog = ( defaultPath: string ): Promise<{