Moves saveAttachment to a redux action

This commit is contained in:
Josh Perez 2022-12-14 13:12:04 -05:00 committed by GitHub
parent c8068cd501
commit b138774454
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 190 additions and 216 deletions

View file

@ -158,7 +158,6 @@ import type AccountManager from './textsecure/AccountManager';
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate'; import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
import { StoryViewModeType, StoryViewTargetType } from './types/Stories'; import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
import { downloadOnboardingStory } from './util/downloadOnboardingStory'; import { downloadOnboardingStory } from './util/downloadOnboardingStory';
import { saveAttachmentFromMessage } from './util/saveAttachment';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -1661,7 +1660,9 @@ export async function startApp(): Promise<void> {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
saveAttachmentFromMessage(selectedMessage); window.reduxActions.conversations.saveAttachmentFromMessage(
selectedMessage
);
return; return;
} }
} }

View file

@ -41,6 +41,7 @@ type PropsType = {
isMaximized: boolean; isMaximized: boolean;
isFullScreen: boolean; isFullScreen: boolean;
menuOptions: MenuOptionsType; menuOptions: MenuOptionsType;
openFileInFolder: (target: string) => unknown;
hasCustomTitleBar: boolean; hasCustomTitleBar: boolean;
hideMenuBar: boolean; hideMenuBar: boolean;
@ -73,6 +74,7 @@ export function App({
hasCustomTitleBar, hasCustomTitleBar,
menuOptions, menuOptions,
openInbox, openInbox,
openFileInFolder,
registerSingleDevice, registerSingleDevice,
renderCallManager, renderCallManager,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
@ -178,7 +180,12 @@ export function App({
'dark-theme': theme === ThemeType.dark, 'dark-theme': theme === ThemeType.dark,
})} })}
> >
<ToastManager hideToast={hideToast} i18n={i18n} toast={toast} /> <ToastManager
hideToast={hideToast}
i18n={i18n}
openFileInFolder={openFileInFolder}
toast={toast}
/>
{renderGlobalModalContainer()} {renderGlobalModalContainer()}
{renderCallManager()} {renderCallManager()}
{renderLightbox()} {renderLightbox()}

View file

@ -30,8 +30,9 @@ export function AvatarLightbox({
<Lightbox <Lightbox
closeLightbox={onClose} closeLightbox={onClose}
i18n={i18n} i18n={i18n}
media={[]}
isViewOnce isViewOnce
media={[]}
saveAttachment={noop}
toggleForwardMessageModal={noop} toggleForwardMessageModal={noop}
> >
<AvatarPreview <AvatarPreview

View file

@ -59,6 +59,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
i18n, i18n,
isViewOnce: Boolean(overrideProps.isViewOnce), isViewOnce: Boolean(overrideProps.isViewOnce),
media: overrideProps.media || [], media: overrideProps.media || [],
saveAttachment: action('saveAttachment'),
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
toggleForwardMessageModal: action('toggleForwardMessageModal'), toggleForwardMessageModal: action('toggleForwardMessageModal'),
}); });

View file

@ -9,7 +9,10 @@ import { createPortal } from 'react-dom';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { useSpring, animated, to } from '@react-spring/web'; 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 { LocalizerType } from '../types/Util';
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
import * as GoogleChrome from '../util/GoogleChrome'; import * as GoogleChrome from '../util/GoogleChrome';
@ -18,7 +21,6 @@ import { Avatar, AvatarSize } from './Avatar';
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import { formatDuration } from '../util/formatDuration'; import { formatDuration } from '../util/formatDuration';
import { isGIF } from '../types/Attachment'; import { isGIF } from '../types/Attachment';
import { saveAttachment } from '../util/saveAttachment';
import { useRestoreFocus } from '../hooks/useRestoreFocus'; import { useRestoreFocus } from '../hooks/useRestoreFocus';
export type PropsType = { export type PropsType = {
@ -28,6 +30,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
isViewOnce?: boolean; isViewOnce?: boolean;
media: Array<MediaItemType>; media: Array<MediaItemType>;
saveAttachment: SaveAttachmentActionCreatorType;
selectedIndex?: number; selectedIndex?: number;
toggleForwardMessageModal: (messageId: string) => unknown; toggleForwardMessageModal: (messageId: string) => unknown;
}; };
@ -53,6 +56,7 @@ export function Lightbox({
media, media,
i18n, i18n,
isViewOnce = false, isViewOnce = false,
saveAttachment,
selectedIndex: initialSelectedIndex = 0, selectedIndex: initialSelectedIndex = 0,
toggleForwardMessageModal, toggleForwardMessageModal,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {

View file

@ -15,7 +15,7 @@ import { SendStatus } from '../messages/MessageSendState';
import { Theme } from '../util/theme'; import { Theme } from '../util/theme';
import { formatDateTimeLong } from '../util/timestamp'; import { formatDateTimeLong } from '../util/timestamp';
import { DurationInSeconds } from '../util/durations'; 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 type { AttachmentType } from '../types/Attachment';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import { Time } from './Time'; import { Time } from './Time';
@ -27,7 +27,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
isInternalUser?: boolean; isInternalUser?: boolean;
onClose: () => unknown; onClose: () => unknown;
saveAttachment: typeof saveAttachment; saveAttachment: SaveAttachmentActionCreatorType;
sender: StoryViewType['sender']; sender: StoryViewType['sender'];
sendState?: Array<StorySendStateType>; sendState?: Array<StorySendStateType>;
attachment?: AttachmentType; attachment?: AttachmentType;

View file

@ -12,7 +12,10 @@ import React, {
import classNames from 'classnames'; import classNames from 'classnames';
import type { DraftBodyRangesType, LocalizerType } from '../types/Util'; import type { DraftBodyRangesType, LocalizerType } from '../types/Util';
import type { ContextMenuOptionType } from './ContextMenu'; 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 { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
@ -44,7 +47,6 @@ import { ToastType } from '../state/ducks/toast';
import { getAvatarColor } from '../types/Colors'; import { getAvatarColor } from '../types/Colors';
import { getStoryBackground } from '../util/getStoryBackground'; import { getStoryBackground } from '../util/getStoryBackground';
import { getStoryDuration } from '../util/getStoryDuration'; import { getStoryDuration } from '../util/getStoryDuration';
import type { saveAttachment } from '../util/saveAttachment';
import { isVideoAttachment } from '../types/Attachment'; import { isVideoAttachment } from '../types/Attachment';
import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice'; import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
@ -100,7 +102,7 @@ export type PropsType = {
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType; replyState?: ReplyStateType;
retrySend: (messageId: string) => unknown; retrySend: (messageId: string) => unknown;
saveAttachment: typeof saveAttachment; saveAttachment: SaveAttachmentActionCreatorType;
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown; setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
showToast: ShowToastActionCreatorType; showToast: ShowToastActionCreatorType;
skinTone?: number; skinTone?: number;

View file

@ -62,6 +62,7 @@ const MESSAGE_DEFAULT_PROPS = {
openLink: shouldNeverBeCalled, openLink: shouldNeverBeCalled,
previews: [], previews: [],
renderAudioAttachment: () => <div />, renderAudioAttachment: () => <div />,
saveAttachment: shouldNeverBeCalled,
scrollToQuotedMessage: shouldNeverBeCalled, scrollToQuotedMessage: shouldNeverBeCalled,
showContactDetail: shouldNeverBeCalled, showContactDetail: shouldNeverBeCalled,
showContactModal: shouldNeverBeCalled, showContactModal: shouldNeverBeCalled,

View file

@ -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 {...defaultProps} />
);
_ToastDangerousFileType.story = {
name: 'ToastDangerousFileType',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('dangerousFileType')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastFileSaved.story = {
name: 'ToastFileSaved',
};

View file

@ -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 (
<Toast
onClose={onClose}
toastAction={{
label: i18n('attachmentSavedShow'),
onClick: onOpenFile,
}}
>
{i18n('attachmentSaved')}
</Toast>
);
}

View file

@ -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({}); export const FileSize = Template.bind({});
FileSize.args = { FileSize.args = {
toast: { toast: {

View file

@ -12,6 +12,7 @@ import { missingCaseError } from '../util/missingCaseError';
export type PropsType = { export type PropsType = {
hideToast: () => unknown; hideToast: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
openFileInFolder: (target: string) => unknown;
toast?: { toast?: {
toastType: ToastType; toastType: ToastType;
parameters?: ReplacementValuesType; parameters?: ReplacementValuesType;
@ -23,6 +24,7 @@ const SHORT_TIMEOUT = 3 * SECOND;
export function ToastManager({ export function ToastManager({
hideToast, hideToast,
i18n, i18n,
openFileInFolder,
toast, toast,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
if (toast === undefined) { if (toast === undefined) {
@ -117,6 +119,24 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.FileSaved) {
return (
<Toast
onClose={hideToast}
toastAction={{
label: i18n('attachmentSavedShow'),
onClick: () => {
if (toast.parameters && 'fullPath' in toast.parameters) {
openFileInFolder(String(toast.parameters.fullPath));
}
},
}}
>
{i18n('attachmentSaved')}
</Toast>
);
}
if (toastType === ToastType.FileSize) { if (toastType === ToastType.FileSize) {
return ( return (
<Toast onClose={hideToast}> <Toast onClose={hideToast}>

View file

@ -14,6 +14,7 @@ import type {
ConversationType, ConversationType,
ConversationTypeType, ConversationTypeType,
InteractionModeType, InteractionModeType,
SaveAttachmentActionCreatorType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories'; import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
import type { ReadStatus } from '../../messages/MessageReadStatus'; import type { ReadStatus } from '../../messages/MessageReadStatus';
@ -87,7 +88,6 @@ import { PaymentEventKind } from '../../types/Payment';
import type { AnyPaymentEvent } from '../../types/Payment'; import type { AnyPaymentEvent } from '../../types/Payment';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { getPaymentEventDescription } from '../../messages/helpers'; import { getPaymentEventDescription } from '../../messages/helpers';
import { saveAttachment } from '../../util/saveAttachment';
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10; const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18; const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
@ -319,6 +319,7 @@ export type PropsActions = {
messageId: string; messageId: string;
}) => void; }) => void;
markViewed(messageId: string): void; markViewed(messageId: string): void;
saveAttachment: SaveAttachmentActionCreatorType;
showLightbox: (options: { showLightbox: (options: {
attachment: AttachmentType; attachment: AttachmentType;
messageId: string; messageId: string;
@ -2380,8 +2381,13 @@ export class Message extends React.PureComponent<Props, State> {
}; };
public openGenericAttachment = (event?: React.MouseEvent): void => { public openGenericAttachment = (event?: React.MouseEvent): void => {
const { id, attachments, timestamp, kickOffAttachmentDownload } = const {
this.props; id,
attachments,
saveAttachment,
timestamp,
kickOffAttachmentDownload,
} = this.props;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();

View file

@ -82,6 +82,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
openLink: action('openLink'), openLink: action('openLink'),
renderAudioAttachment: () => <div>*AudioAttachment*</div>, renderAudioAttachment: () => <div>*AudioAttachment*</div>,
saveAttachment: action('saveAttachment'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(

View file

@ -96,6 +96,7 @@ export type PropsReduxActions = Pick<
| 'checkForAccount' | 'checkForAccount'
| 'clearSelectedMessage' | 'clearSelectedMessage'
| 'doubleCheckMissingQuoteReference' | 'doubleCheckMissingQuoteReference'
| 'saveAttachment'
| 'showContactModal' | 'showContactModal'
| 'showLightbox' | 'showLightbox'
| 'showLightboxForViewOnceMedia' | 'showLightboxForViewOnceMedia'
@ -293,6 +294,7 @@ export class MessageDetail extends React.Component<Props> {
openGiftBadge, openGiftBadge,
openLink, openLink,
renderAudioAttachment, renderAudioAttachment,
saveAttachment,
showContactDetail, showContactDetail,
showContactModal, showContactModal,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
@ -338,6 +340,7 @@ export class MessageDetail extends React.Component<Props> {
openGiftBadge={openGiftBadge} openGiftBadge={openGiftBadge}
openLink={openLink} openLink={openLink}
renderAudioAttachment={renderAudioAttachment} renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}
shouldCollapseAbove={false} shouldCollapseAbove={false}
shouldCollapseBelow={false} shouldCollapseBelow={false}
shouldHideMetadata={false} shouldHideMetadata={false}

View file

@ -124,6 +124,7 @@ const defaultMessageProps: TimelineMessagesProps = {
setQuoteByMessageId: action('default--setQuoteByMessageId'), setQuoteByMessageId: action('default--setQuoteByMessageId'),
retrySend: action('default--retrySend'), retrySend: action('default--retrySend'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
saveAttachment: action('saveAttachment'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
selectMessage: action('default--selectMessage'), selectMessage: action('default--selectMessage'),
shouldCollapseAbove: false, shouldCollapseAbove: false,

View file

@ -283,6 +283,7 @@ const actions = () => ({
deleteMessageForEveryone: action('deleteMessageForEveryone'), deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'), openConversation: action('openConversation'),
saveAttachment: action('saveAttachment'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'),

View file

@ -253,6 +253,7 @@ const getActions = createSelector(
'kickOffAttachmentDownload', 'kickOffAttachmentDownload',
'markAttachmentAsCorrupted', 'markAttachmentAsCorrupted',
'messageExpanded', 'messageExpanded',
'saveAttachment',
'showLightbox', 'showLightbox',
'showLightboxForViewOnceMedia', 'showLightboxForViewOnceMedia',
'openLink', 'openLink',

View file

@ -80,6 +80,7 @@ const getDefaultProps = () => ({
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'), openConversation: action('openConversation'),
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
saveAttachment: action('saveAttachment'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
showLightbox: action('showLightbox'), showLightbox: action('showLightbox'),

View file

@ -293,6 +293,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
renderEmojiPicker, renderEmojiPicker,
renderReactionPicker, renderReactionPicker,
renderAudioAttachment, renderAudioAttachment,
saveAttachment: action('saveAttachment'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),

View file

@ -27,7 +27,6 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore';
import type { Props as ReactionPickerProps } from './ReactionPicker'; import type { Props as ReactionPickerProps } from './ReactionPicker';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
import { saveAttachment } from '../../util/saveAttachment';
export type PropsData = { export type PropsData = {
canDownload: boolean; canDownload: boolean;
@ -172,7 +171,7 @@ export function TimelineMessage(props: Props): JSX.Element {
}); });
const openGenericAttachment = (event?: React.MouseEvent): void => { const openGenericAttachment = (event?: React.MouseEvent): void => {
const { kickOffAttachmentDownload } = props; const { kickOffAttachmentDownload, saveAttachment } = props;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();

View file

@ -122,7 +122,6 @@ type MigrationsModuleType = {
loadStickerData: ( loadStickerData: (
sticker: StickerType | undefined sticker: StickerType | undefined
) => Promise<StickerWithHydratedData | undefined>; ) => Promise<StickerWithHydratedData | undefined>;
openFileInFolder: (target: string) => Promise<void>;
readAttachmentData: (path: string) => Promise<Uint8Array>; readAttachmentData: (path: string) => Promise<Uint8Array>;
readDraftData: (path: string) => Promise<Uint8Array>; readDraftData: (path: string) => Promise<Uint8Array>;
readStickerData: (path: string) => Promise<Uint8Array>; readStickerData: (path: string) => Promise<Uint8Array>;
@ -185,7 +184,6 @@ export function initializeMigrations({
getStickersPath, getStickersPath,
getBadgesPath, getBadgesPath,
getTempPath, getTempPath,
openFileInFolder,
saveAttachmentToDisk, saveAttachmentToDisk,
} = Attachments; } = Attachments;
const { const {
@ -266,7 +264,6 @@ export function initializeMigrations({
loadPreviewData, loadPreviewData,
loadQuoteData, loadQuoteData,
loadStickerData, loadStickerData,
openFileInFolder,
readAttachmentData, readAttachmentData,
readDraftData, readDraftData,
readStickerData, readStickerData,
@ -364,7 +361,6 @@ type AttachmentsModuleType = {
) => (relativePath: string) => string; ) => (relativePath: string) => string;
createDoesExist: (root: string) => (relativePath: string) => Promise<boolean>; createDoesExist: (root: string) => (relativePath: string) => Promise<boolean>;
openFileInFolder: (target: string) => Promise<void>;
saveAttachmentToDisk: ({ saveAttachmentToDisk: ({
data, data,
name, name,

View file

@ -21,6 +21,8 @@ import { getOwn } from '../../util/getOwn';
import { assertDev, strictAssert } from '../../util/assert'; import { assertDev, strictAssert } from '../../util/assert';
import type { DurationInSeconds } from '../../util/durations'; import type { DurationInSeconds } from '../../util/durations';
import * as universalExpireTimer from '../../util/universalExpireTimer'; import * as universalExpireTimer from '../../util/universalExpireTimer';
import * as Attachment from '../../types/Attachment';
import { isFileDangerous } from '../../util/isFileDangerous';
import type { import type {
ShowSendAnywayDialogActionType, ShowSendAnywayDialogActionType,
ToggleProfileEditorErrorActionType, ToggleProfileEditorErrorActionType,
@ -901,6 +903,8 @@ export const actions = {
reviewGroupMemberNameCollision, reviewGroupMemberNameCollision,
reviewMessageRequestNameCollision, reviewMessageRequestNameCollision,
revokePendingMembershipsFromGroupV2, revokePendingMembershipsFromGroupV2,
saveAttachment,
saveAttachmentFromMessage,
saveAvatarToDisk, saveAvatarToDisk,
scrollToMessage, scrollToMessage,
selectMessage, 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<void, RootStateType, unknown, ShowToastActionType> {
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<void, RootStateType, unknown, ShowToastActionType> {
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 { function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' }; return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
} }

View file

@ -19,7 +19,7 @@ import {
} from '../../util/GoogleChrome'; } from '../../util/GoogleChrome';
import { isTapToView } from '../selectors/message'; import { isTapToView } from '../selectors/message';
import { SHOW_TOAST, ToastType } from './toast'; import { SHOW_TOAST, ToastType } from './toast';
import { saveAttachmentFromMessage } from '../../util/saveAttachment'; import { saveAttachmentFromMessage } from './conversations';
import { showStickerPackPreview } from './globalModals'; import { showStickerPackPreview } from './globalModals';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
@ -213,7 +213,7 @@ function showLightbox(opts: {
| ShowStickerPackPreviewActionType | ShowStickerPackPreviewActionType
| ShowToastActionType | ShowToastActionType
> { > {
return async dispatch => { return async (dispatch, getState) => {
const { attachment, messageId } = opts; const { attachment, messageId } = opts;
const message = await getMessageById(messageId); const message = await getMessageById(messageId);
@ -233,7 +233,11 @@ function showLightbox(opts: {
!isImageTypeSupported(contentType) && !isImageTypeSupported(contentType) &&
!isVideoTypeSupported(contentType) !isVideoTypeSupported(contentType)
) { ) {
await saveAttachmentFromMessage(messageId, attachment); saveAttachmentFromMessage(messageId, attachment)(
dispatch,
getState,
null
);
return; return;
} }

View file

@ -1,9 +1,12 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions'; import type { NoopActionType } from './noop';
import type { ReplacementValuesType } from '../../types/Util'; import type { ReplacementValuesType } from '../../types/Util';
import { useBoundActions } from '../../hooks/useBoundActions';
export enum ToastType { export enum ToastType {
AddingUserToGroup = 'AddingUserToGroup', AddingUserToGroup = 'AddingUserToGroup',
@ -18,6 +21,7 @@ export enum ToastType {
Error = 'Error', Error = 'Error',
Expired = 'Expired', Expired = 'Expired',
FailedToDeleteUsername = 'FailedToDeleteUsername', FailedToDeleteUsername = 'FailedToDeleteUsername',
FileSaved = 'FileSaved',
FileSize = 'FileSize', FileSize = 'FileSize',
InvalidConversation = 'InvalidConversation', InvalidConversation = 'InvalidConversation',
LeftGroup = 'LeftGroup', 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 = ( export type ShowToastActionCreatorType = (
toastType: ToastType, toastType: ToastType,
parameters?: ReplacementValuesType parameters?: ReplacementValuesType
@ -92,6 +104,7 @@ export const showToast: ShowToastActionCreatorType = (
export const actions = { export const actions = {
hideToast, hideToast,
openFileInFolder,
showToast, showToast,
}; };

View file

@ -11,6 +11,7 @@ import type { StateType } from '../reducer';
import { Lightbox } from '../../components/Lightbox'; import { Lightbox } from '../../components/Lightbox';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox'; import { useLightboxActions } from '../ducks/lightbox';
import { import {
@ -22,6 +23,7 @@ import {
export function SmartLightbox(): JSX.Element | null { export function SmartLightbox(): JSX.Element | null {
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const { saveAttachment } = useConversationsActions();
const { closeLightbox } = useLightboxActions(); const { closeLightbox } = useLightboxActions();
const { toggleForwardMessageModal } = useGlobalModalActions(); const { toggleForwardMessageModal } = useGlobalModalActions();
@ -45,6 +47,7 @@ export function SmartLightbox(): JSX.Element | null {
i18n={i18n} i18n={i18n}
isViewOnce={isViewOnce} isViewOnce={isViewOnce}
media={media} media={media}
saveAttachment={saveAttachment}
selectedIndex={selectedIndex || 0} selectedIndex={selectedIndex || 0}
toggleForwardMessageModal={toggleForwardMessageModal} toggleForwardMessageModal={toggleForwardMessageModal}
/> />

View file

@ -22,7 +22,6 @@ import {
shouldShowStoriesView, shouldShowStoriesView,
} from '../selectors/stories'; } from '../selectors/stories';
import { retryMessageSend } from '../../util/retryMessageSend'; import { retryMessageSend } from '../../util/retryMessageSend';
import { saveAttachment } from '../../util/saveAttachment';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories'; import { useStoriesActions } from '../ducks/stories';
@ -34,7 +33,8 @@ function renderStoryCreator(): JSX.Element {
export function SmartStories(): JSX.Element | null { export function SmartStories(): JSX.Element | null {
const storiesActions = useStoriesActions(); const storiesActions = useStoriesActions();
const { showConversation, toggleHideStories } = useConversationsActions(); const { saveAttachment, showConversation, toggleHideStories } =
useConversationsActions();
const { showStoriesSettings, toggleForwardMessageModal } = const { showStoriesSettings, toggleForwardMessageModal } =
useGlobalModalActions(); useGlobalModalActions();
const { showToast } = useToastActions(); const { showToast } = useToastActions();

View file

@ -29,7 +29,6 @@ import { isInFullScreenCall } from '../selectors/calling';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { retryMessageSend } from '../../util/retryMessageSend'; import { retryMessageSend } from '../../util/retryMessageSend';
import { saveAttachment } from '../../util/saveAttachment';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled'; import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
import { useActions as useEmojisActions } from '../ducks/emojis'; import { useActions as useEmojisActions } from '../ducks/emojis';
@ -42,7 +41,8 @@ import { useIsWindowActive } from '../../hooks/useIsWindowActive';
export function SmartStoryViewer(): JSX.Element | null { export function SmartStoryViewer(): JSX.Element | null {
const storiesActions = useStoriesActions(); const storiesActions = useStoriesActions();
const { onUseEmoji } = useEmojisActions(); const { onUseEmoji } = useEmojisActions();
const { showConversation, toggleHideStories } = useConversationsActions(); const { saveAttachment, showConversation, toggleHideStories } =
useConversationsActions();
const { onSetSkinTone } = useItemsActions(); const { onSetSkinTone } = useItemsActions();
const { showToast } = useToastActions(); const { showToast } = useToastActions();

View file

@ -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<void> {
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<void> {
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);
}

View file

@ -22,10 +22,6 @@ import type {
ToastInternalError, ToastInternalError,
ToastPropsType as ToastInternalErrorPropsType, ToastPropsType as ToastInternalErrorPropsType,
} from '../components/ToastInternalError'; } from '../components/ToastInternalError';
import type {
ToastFileSaved,
ToastPropsType as ToastFileSavedPropsType,
} from '../components/ToastFileSaved';
import type { import type {
ToastFileSize, ToastFileSize,
ToastPropsType as ToastFileSizePropsType, ToastPropsType as ToastFileSizePropsType,
@ -61,10 +57,6 @@ export function showToast(
Toast: typeof ToastInternalError, Toast: typeof ToastInternalError,
props: ToastInternalErrorPropsType props: ToastInternalErrorPropsType
): void; ): void;
export function showToast(
Toast: typeof ToastFileSaved,
props: ToastFileSavedPropsType
): void;
export function showToast( export function showToast(
Toast: typeof ToastFileSize, Toast: typeof ToastFileSize,
props: ToastFileSizePropsType props: ToastFileSizePropsType

View file

@ -52,7 +52,6 @@ import {
removeLinkPreview, removeLinkPreview,
suspendLinkPreviews, suspendLinkPreviews,
} from '../services/LinkPreview'; } from '../services/LinkPreview';
import { saveAttachment } from '../util/saveAttachment';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { startConversation } from '../util/startConversation'; import { startConversation } from '../util/startConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
@ -749,7 +748,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}: ItemClickEvent) => { }: ItemClickEvent) => {
switch (type) { switch (type) {
case 'documents': { case 'documents': {
saveAttachment(attachment, message.sent_at); window.reduxActions.conversations.saveAttachment(
attachment,
message.sent_at
);
break; break;
} }

View file

@ -197,10 +197,6 @@ export const createDoesExist = (
}; };
}; };
export const openFileInFolder = async (target: string): Promise<void> => {
ipcRenderer.send('show-item-in-folder', target);
};
const showSaveDialog = ( const showSaveDialog = (
defaultPath: string defaultPath: string
): Promise<{ ): Promise<{