Allow downloading multiple images into one directory

Co-authored-by: Major-Mayer <lrdarknesss@yahoo.de>
This commit is contained in:
Scott Nonnenberg 2024-10-24 07:44:12 +10:00 committed by GitHub
parent 35946ef53c
commit 76e2597d30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 282 additions and 24 deletions

View file

@ -791,9 +791,15 @@
"icu:attachmentSaved": { "icu:attachmentSaved": {
"messageformat": "Anhang gespeichert." "messageformat": "Anhang gespeichert."
}, },
"icu:attachmentSavedPlural": {
"messageformat": "{count, plural, one {Anhang} other {# Anhänge}} gespeichert."
},
"icu:attachmentSavedShow": { "icu:attachmentSavedShow": {
"messageformat": "Im Ordner anzeigen" "messageformat": "Im Ordner anzeigen"
}, },
"icu:attachmentStillDownloading": {
"messageformat": "{count, plural, one {Anhang kann} other {Anhänge können}} nicht gespeichert werden, da {count, plural, one {er} other {#}} noch nicht fertig heruntergeladen {count, plural, one {wurde} other {wurden}}."
},
"icu:you": { "icu:you": {
"messageformat": "Du" "messageformat": "Du"
}, },

View file

@ -1066,10 +1066,18 @@
"messageformat": "Attachment saved.", "messageformat": "Attachment saved.",
"description": "Shown after user selects to save to downloads" "description": "Shown after user selects to save to downloads"
}, },
"icu:attachmentSavedPlural": {
"messageformat": "{count, plural, one {Attachment} other {# attachments}} saved",
"description": "Shown after user selects to save to downloads"
},
"icu:attachmentSavedShow": { "icu:attachmentSavedShow": {
"messageformat": "Show in folder", "messageformat": "Show in folder",
"description": "Button label for showing the attachment in your file system" "description": "Button label for showing the attachment in your file system"
}, },
"icu:attachmentStillDownloading": {
"messageformat": "Can't save {count, plural, one {attachment, since it hasn't} other {attachments, since # haven't}} finished downloading yet",
"description": "Shown when the user tries to save an attachment to the filesystem, but it's impossible since the download from servers hasn't finished yet"
},
"icu:you": { "icu:you": {
"messageformat": "You", "messageformat": "You",
"description": "Shown when the user represented is the current user." "description": "Shown when the user represented is the current user."

View file

@ -3032,6 +3032,32 @@ ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
return { canceled: false, filePath: finalFilePath }; return { canceled: false, filePath: finalFilePath };
}); });
ipc.handle('show-save-multi-dialog', async _event => {
if (!mainWindow) {
getLogger().warn('show-save-multi-dialog: no main window');
return { canceled: true };
}
const { canceled, filePaths: selectedDirPaths } = await dialog.showOpenDialog(
mainWindow,
{
defaultPath: app.getPath('downloads'),
properties: ['openDirectory', 'createDirectory'],
}
);
if (canceled || selectedDirPaths.length === 0) {
return { canceled: true };
}
if (selectedDirPaths.length > 1) {
getLogger().warn('show-save-multi-dialog: multiple directories selected');
return { canceled: true };
}
return { canceled: false, dirPath: selectedDirPaths[0] };
});
ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => { ipc.handle('executeMenuRole', async ({ sender }, untypedRole) => {
const role = untypedRole as MenuItemConstructorOptions['role']; const role = untypedRole as MenuItemConstructorOptions['role'];

View file

@ -55,6 +55,7 @@ const MESSAGE_DEFAULT_PROPS = {
renderAudioAttachment: () => <div />, renderAudioAttachment: () => <div />,
renderingContext: 'EditHistoryMessagesModal', renderingContext: 'EditHistoryMessagesModal',
saveAttachment: shouldNeverBeCalled, saveAttachment: shouldNeverBeCalled,
saveAttachments: shouldNeverBeCalled,
scrollToQuotedMessage: shouldNeverBeCalled, scrollToQuotedMessage: shouldNeverBeCalled,
shouldCollapseAbove: false, shouldCollapseAbove: false,
shouldCollapseBelow: false, shouldCollapseBelow: false,
@ -62,6 +63,7 @@ const MESSAGE_DEFAULT_PROPS = {
showContactModal: shouldNeverBeCalled, showContactModal: shouldNeverBeCalled,
showConversation: noop, showConversation: noop,
showEditHistoryModal: noop, showEditHistoryModal: noop,
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled, showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled, showLightboxForViewOnceMedia: shouldNeverBeCalled,

View file

@ -64,8 +64,10 @@ const MESSAGE_DEFAULT_PROPS = {
pushPanelForConversation: shouldNeverBeCalled, pushPanelForConversation: shouldNeverBeCalled,
renderAudioAttachment: () => <div />, renderAudioAttachment: () => <div />,
saveAttachment: shouldNeverBeCalled, saveAttachment: shouldNeverBeCalled,
saveAttachments: shouldNeverBeCalled,
scrollToQuotedMessage: shouldNeverBeCalled, scrollToQuotedMessage: shouldNeverBeCalled,
showConversation: noop, showConversation: noop,
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled, showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled, showLightbox: shouldNeverBeCalled,

View file

@ -27,6 +27,13 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.AlreadyGroupMember }; return { toastType: ToastType.AlreadyGroupMember };
case ToastType.AlreadyRequestedToJoin: case ToastType.AlreadyRequestedToJoin:
return { toastType: ToastType.AlreadyRequestedToJoin }; return { toastType: ToastType.AlreadyRequestedToJoin };
case ToastType.AttachmentDownloadStillInProgress:
return {
toastType: ToastType.AttachmentDownloadStillInProgress,
parameters: {
count: 1,
},
};
case ToastType.Blocked: case ToastType.Blocked:
return { toastType: ToastType.Blocked }; return { toastType: ToastType.Blocked };
case ToastType.BlockedGroup: case ToastType.BlockedGroup:

View file

@ -88,6 +88,16 @@ export function renderToast({
); );
} }
if (toastType === ToastType.AttachmentDownloadStillInProgress) {
return (
<Toast onClose={hideToast}>
{i18n('icu:attachmentStillDownloading', {
count: toast.parameters.count,
})}
</Toast>
);
}
if (toastType === ToastType.Blocked) { if (toastType === ToastType.Blocked) {
return <Toast onClose={hideToast}>{i18n('icu:unblockToSend')}</Toast>; return <Toast onClose={hideToast}>{i18n('icu:unblockToSend')}</Toast>;
} }
@ -310,7 +320,9 @@ export function renderToast({
}, },
}} }}
> >
{i18n('icu:attachmentSaved')} {i18n('icu:attachmentSavedPlural', {
count: toast.parameters.countOfFiles ?? 1,
})}
</Toast> </Toast>
); );
} }

View file

@ -24,6 +24,7 @@ import type {
InteractionModeType, InteractionModeType,
PushPanelForConversationActionType, PushPanelForConversationActionType,
SaveAttachmentActionCreatorType, SaveAttachmentActionCreatorType,
SaveAttachmentsActionCreatorType,
ShowConversationType, ShowConversationType,
} from '../../state/ducks/conversations'; } from '../../state/ducks/conversations';
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories'; import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
@ -352,6 +353,7 @@ export type PropsActions = {
messageId: string; messageId: string;
}) => void; }) => void;
saveAttachment: SaveAttachmentActionCreatorType; saveAttachment: SaveAttachmentActionCreatorType;
saveAttachments: SaveAttachmentsActionCreatorType;
showLightbox: (options: { showLightbox: (options: {
attachment: AttachmentType; attachment: AttachmentType;
messageId: string; messageId: string;
@ -366,6 +368,7 @@ export type PropsActions = {
targetMessage?: (messageId: string, conversationId: string) => unknown; targetMessage?: (messageId: string, conversationId: string) => unknown;
showEditHistoryModal?: (id: string) => unknown; showEditHistoryModal?: (id: string) => unknown;
showAttachmentDownloadStillInProgressToast: (count: number) => unknown;
showExpiredIncomingTapToViewToast: () => unknown; showExpiredIncomingTapToViewToast: () => unknown;
showExpiredOutgoingTapToViewToast: () => unknown; showExpiredOutgoingTapToViewToast: () => unknown;
viewStory: ViewStoryActionCreatorType; viewStory: ViewStoryActionCreatorType;

View file

@ -95,9 +95,11 @@ export type PropsReduxActions = Pick<
| 'pushPanelForConversation' | 'pushPanelForConversation'
| 'retryMessageSend' | 'retryMessageSend'
| 'saveAttachment' | 'saveAttachment'
| 'saveAttachments'
| 'showContactModal' | 'showContactModal'
| 'showConversation' | 'showConversation'
| 'showEditHistoryModal' | 'showEditHistoryModal'
| 'showAttachmentDownloadStillInProgressToast'
| 'showExpiredIncomingTapToViewToast' | 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast' | 'showExpiredOutgoingTapToViewToast'
| 'showLightbox' | 'showLightbox'
@ -139,9 +141,11 @@ export function MessageDetail({
retryMessageSend, retryMessageSend,
renderAudioAttachment, renderAudioAttachment,
saveAttachment, saveAttachment,
saveAttachments,
showContactModal, showContactModal,
showConversation, showConversation,
showEditHistoryModal, showEditHistoryModal,
showAttachmentDownloadStillInProgressToast,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showLightbox, showLightbox,
@ -348,6 +352,7 @@ export function MessageDetail({
retryMessageSend={retryMessageSend} retryMessageSend={retryMessageSend}
renderAudioAttachment={renderAudioAttachment} renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment} saveAttachment={saveAttachment}
saveAttachments={saveAttachments}
shouldCollapseAbove={false} shouldCollapseAbove={false}
shouldCollapseBelow={false} shouldCollapseBelow={false}
shouldHideMetadata={false} shouldHideMetadata={false}
@ -357,6 +362,9 @@ export function MessageDetail({
log.warn('MessageDetail: scrollToQuotedMessage called!'); log.warn('MessageDetail: scrollToQuotedMessage called!');
}} }}
showContactModal={showContactModal} showContactModal={showContactModal}
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showExpiredIncomingTapToViewToast={ showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast showExpiredIncomingTapToViewToast
} }

View file

@ -125,6 +125,7 @@ const defaultMessageProps: TimelineMessagesProps = {
copyMessageText: action('copyMessageText'), copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
saveAttachments: action('saveAttachments'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
targetMessage: action('default--targetMessage'), targetMessage: action('default--targetMessage'),
shouldCollapseAbove: false, shouldCollapseAbove: false,
@ -133,6 +134,9 @@ const defaultMessageProps: TimelineMessagesProps = {
showSpoiler: action('showSpoiler'), showSpoiler: action('showSpoiler'),
pushPanelForConversation: action('default--pushPanelForConversation'), pushPanelForConversation: action('default--pushPanelForConversation'),
showContactModal: action('default--showContactModal'), showContactModal: action('default--showContactModal'),
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast' 'showExpiredIncomingTapToViewToast'
), ),

View file

@ -291,6 +291,7 @@ const actions = () => ({
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
saveAttachments: action('saveAttachments'),
pushPanelForConversation: action('pushPanelForConversation'), pushPanelForConversation: action('pushPanelForConversation'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
@ -305,6 +306,9 @@ const actions = () => ({
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
scrollToQuotedMessage: action('scrollToQuotedMessage'), scrollToQuotedMessage: action('scrollToQuotedMessage'),
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast' 'showExpiredIncomingTapToViewToast'
), ),

View file

@ -82,6 +82,7 @@ const getDefaultProps = () => ({
showConversation: action('showConversation'), showConversation: action('showConversation'),
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
saveAttachments: action('saveAttachments'),
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'), onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
onOutgoingAudioCallInConversation: action( onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation' 'onOutgoingAudioCallInConversation'
@ -96,6 +97,9 @@ const getDefaultProps = () => ({
toggleForwardMessagesModal: action('toggleForwardMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'), showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'), doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast' 'showExpiredIncomingTapToViewToast'
), ),

View file

@ -317,6 +317,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
renderReactionPicker, renderReactionPicker,
renderAudioAttachment, renderAudioAttachment,
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
saveAttachments: action('saveAttachments'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
copyMessageText: action('copyMessageText'), copyMessageText: action('copyMessageText'),
@ -340,6 +341,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showSpoiler: action('showSpoiler'), showSpoiler: action('showSpoiler'),
pushPanelForConversation: action('pushPanelForConversation'), pushPanelForConversation: action('pushPanelForConversation'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
showAttachmentDownloadStillInProgressToast: action(
'showAttachmentDownloadStillInProgressToast'
),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast' 'showExpiredIncomingTapToViewToast'
), ),

View file

@ -121,6 +121,8 @@ export function TimelineMessage(props: Props): JSX.Element {
retryDeleteForEveryone, retryDeleteForEveryone,
retryMessageSend, retryMessageSend,
saveAttachment, saveAttachment,
saveAttachments,
showAttachmentDownloadStillInProgressToast,
selectedReaction, selectedReaction,
setQuoteByMessageId, setQuoteByMessageId,
setMessageToEdit, setMessageToEdit,
@ -212,22 +214,42 @@ export function TimelineMessage(props: Props): JSX.Element {
event.stopPropagation(); event.stopPropagation();
} }
if (!attachments || attachments.length !== 1) { if (!attachments || attachments.length === 0) {
return; return;
} }
const attachment = attachments[0]; let attachmentsInProgress = 0;
// check if any attachment needs to be downloaded from servers
for (const attachment of attachments) {
if (!isDownloaded(attachment)) { if (!isDownloaded(attachment)) {
kickOffAttachmentDownload({ kickOffAttachmentDownload({
attachment, attachment,
messageId: id, messageId: id,
}); });
return;
attachmentsInProgress += 1;
}
} }
saveAttachment(attachment, timestamp); if (attachmentsInProgress !== 0) {
showAttachmentDownloadStillInProgressToast(attachmentsInProgress);
}
if (attachments.length !== 1) {
saveAttachments(attachments, timestamp);
} else {
saveAttachment(attachments[0], timestamp);
}
}, },
[kickOffAttachmentDownload, saveAttachment, attachments, id, timestamp] [
kickOffAttachmentDownload,
saveAttachments,
saveAttachment,
showAttachmentDownloadStillInProgressToast,
attachments,
id,
timestamp,
]
); );
const handleContextMenu = useHandleMessageContextMenu(menuTriggerRef); const handleContextMenu = useHandleMessageContextMenu(menuTriggerRef);
@ -237,16 +259,13 @@ export function TimelineMessage(props: Props): JSX.Element {
const shouldShowAdditional = const shouldShowAdditional =
doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow; doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow;
const multipleAttachments = attachments && attachments.length > 1; const hasPendingAttachments =
const firstAttachment = attachments && attachments[0]; attachments?.length && attachments.some(attachment => attachment.pending);
// If any of the conditions is not given -> undefined is returned
// --> download menu icon is not rendered
const handleDownload = const handleDownload =
canDownload && canDownload && !isSticker && !isTapToView && !hasPendingAttachments
!isSticker &&
!multipleAttachments &&
!isTapToView &&
firstAttachment &&
!firstAttachment.pending
? openGenericAttachment ? openGenericAttachment
: undefined; : undefined;

View file

@ -115,6 +115,7 @@ type MigrationsModuleType = {
saveAttachmentToDisk: (options: { saveAttachmentToDisk: (options: {
data: Uint8Array; data: Uint8Array;
name: string; name: string;
baseDir?: string;
}) => Promise<null | { fullPath: string; name: string }>; }) => Promise<null | { fullPath: string; name: string }>;
processNewAttachment: (attachment: AttachmentType) => Promise<AttachmentType>; processNewAttachment: (attachment: AttachmentType) => Promise<AttachmentType>;
processNewSticker: (stickerData: Uint8Array) => Promise< processNewSticker: (stickerData: Uint8Array) => Promise<
@ -406,9 +407,11 @@ type AttachmentsModuleType = {
saveAttachmentToDisk: ({ saveAttachmentToDisk: ({
data, data,
name, name,
dirName,
}: { }: {
data: Uint8Array; data: Uint8Array;
name: string; name: string;
dirName?: string;
}) => Promise<null | { fullPath: string; name: string }>; }) => Promise<null | { fullPath: string; name: string }>;
ensureAttachmentIsReencryptable: ( ensureAttachmentIsReencryptable: (

View file

@ -15,7 +15,7 @@ import {
} from 'lodash'; } from 'lodash';
import type { PhoneNumber } from 'google-libphonenumber'; import type { PhoneNumber } from 'google-libphonenumber';
import { clipboard } from 'electron'; import { clipboard, ipcRenderer } from 'electron';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { DataReader, DataWriter } from '../../sql/Client'; import { DataReader, DataWriter } from '../../sql/Client';
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
@ -1163,6 +1163,7 @@ export const actions = {
reviewConversationNameCollision, reviewConversationNameCollision,
revokePendingMembershipsFromGroupV2, revokePendingMembershipsFromGroupV2,
saveAttachment, saveAttachment,
saveAttachments,
saveAttachmentFromMessage, saveAttachmentFromMessage,
saveAvatarToDisk, saveAvatarToDisk,
scrollToMessage, scrollToMessage,
@ -1189,6 +1190,7 @@ export const actions = {
setPreJoinConversation, setPreJoinConversation,
setVoiceNotePlaybackRate, setVoiceNotePlaybackRate,
showArchivedConversations, showArchivedConversations,
showAttachmentDownloadStillInProgressToast,
showChooseGroupMembers, showChooseGroupMembers,
showConversation, showConversation,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
@ -3862,6 +3864,105 @@ function saveAttachment(
}; };
} }
const showSaveMultiDialog = (): Promise<{
canceled: boolean;
dirPath?: string;
}> => {
return ipcRenderer.invoke('show-save-multi-dialog');
};
export type SaveAttachmentsActionCreatorType = ReadonlyDeep<
(
attachments: ReadonlyArray<AttachmentType>,
timestamp?: number,
index?: number
) => unknown
>;
function saveAttachments(
attachments: ReadonlyArray<AttachmentType>,
timestamp = Date.now()
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async dispatch => {
// check if any of the attachments could be dangerous
for (const attachment of attachments) {
const { fileName = '' } = attachment;
const isDangerous = isFileDangerous(fileName);
if (isDangerous) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.DangerousFileType,
},
});
return;
}
}
const { canceled, dirPath } = await showSaveMultiDialog();
if (canceled || !dirPath) {
return;
}
const { readAttachmentData, saveAttachmentToDisk } =
window.Signal.Migrations;
let fullPath;
let index = 0;
for (const attachment of attachments) {
index += 1;
// eslint-disable-next-line no-await-in-loop
const result = await Attachment.save({
attachment,
index,
readAttachmentData,
saveAttachmentToDisk,
timestamp,
baseDir: dirPath,
});
if (fullPath === undefined) {
fullPath = result;
}
}
if (fullPath == null) {
throw new Error('saveAttachments: Returned path to attachment is null!');
}
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.FileSaved,
parameters: {
countOfFiles: attachments.length,
fullPath,
},
},
});
};
}
function showAttachmentDownloadStillInProgressToast(
count: number
): ShowToastActionType {
log.info(
`showAttachmentDownloadStillInProgressToast: ${count} still-pending attachments`
);
return {
type: SHOW_TOAST,
payload: {
toastType: ToastType.AttachmentDownloadStillInProgress,
parameters: {
count,
},
},
};
}
// is only used by lightbox/ gallery
export function saveAttachmentFromMessage( export function saveAttachmentFromMessage(
messageId: string, messageId: string,
providedAttachment?: AttachmentType providedAttachment?: AttachmentType

View file

@ -49,7 +49,9 @@ export const SmartMessageDetail = memo(
popPanelForConversation, popPanelForConversation,
pushPanelForConversation, pushPanelForConversation,
saveAttachment, saveAttachment,
saveAttachments,
showConversation, showConversation,
showAttachmentDownloadStillInProgressToast,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showSpoiler, showSpoiler,
@ -99,10 +101,14 @@ export const SmartMessageDetail = memo(
receivedAt={receivedAt} receivedAt={receivedAt}
renderAudioAttachment={renderAudioAttachment} renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment} saveAttachment={saveAttachment}
saveAttachments={saveAttachments}
sentAt={message.timestamp} sentAt={message.timestamp}
showContactModal={showContactModal} showContactModal={showContactModal}
showConversation={showConversation} showConversation={showConversation}
showEditHistoryModal={showEditHistoryModal} showEditHistoryModal={showEditHistoryModal}
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast} showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast} showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox} showLightbox={showLightbox}

View file

@ -126,10 +126,12 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
retryDeleteForEveryone, retryDeleteForEveryone,
retryMessageSend, retryMessageSend,
saveAttachment, saveAttachment,
saveAttachments,
targetMessage, targetMessage,
toggleSelectMessage, toggleSelectMessage,
setMessageToEdit, setMessageToEdit,
showConversation, showConversation,
showAttachmentDownloadStillInProgressToast,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showSpoiler, showSpoiler,
@ -218,12 +220,16 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
retryMessageSend={retryMessageSend} retryMessageSend={retryMessageSend}
returnToActiveCall={returnToActiveCall} returnToActiveCall={returnToActiveCall}
saveAttachment={saveAttachment} saveAttachment={saveAttachment}
saveAttachments={saveAttachments}
scrollToQuotedMessage={scrollToQuotedMessage} scrollToQuotedMessage={scrollToQuotedMessage}
targetMessage={targetMessage} targetMessage={targetMessage}
setQuoteByMessageId={setQuoteByMessageId} setQuoteByMessageId={setQuoteByMessageId}
setMessageToEdit={setMessageToEdit} setMessageToEdit={setMessageToEdit}
showContactModal={showContactModal} showContactModal={showContactModal}
showConversation={showConversation} showConversation={showConversation}
showAttachmentDownloadStillInProgressToast={
showAttachmentDownloadStillInProgressToast
}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast} showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast} showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox} showLightbox={showLightbox}

View file

@ -983,6 +983,7 @@ export const save = async ({
readAttachmentData, readAttachmentData,
saveAttachmentToDisk, saveAttachmentToDisk,
timestamp, timestamp,
baseDir,
}: { }: {
attachment: AttachmentType; attachment: AttachmentType;
index?: number; index?: number;
@ -992,8 +993,14 @@ export const save = async ({
saveAttachmentToDisk: (options: { saveAttachmentToDisk: (options: {
data: Uint8Array; data: Uint8Array;
name: string; name: string;
baseDir?: string;
}) => Promise<{ name: string; fullPath: string } | null>; }) => Promise<{ name: string; fullPath: string } | null>;
timestamp?: number; timestamp?: number;
/**
* Base directory for saving the attachment.
* If omitted, a dialog will be opened to let the user choose a directory
*/
baseDir?: string;
}): Promise<string | null> => { }): Promise<string | null> => {
let data: Uint8Array; let data: Uint8Array;
if (attachment.path) { if (attachment.path) {
@ -1009,6 +1016,7 @@ export const save = async ({
const result = await saveAttachmentToDisk({ const result = await saveAttachmentToDisk({
data, data,
name, name,
baseDir,
}); });
if (!result) { if (!result) {

View file

@ -6,6 +6,7 @@ export enum ToastType {
AddedUsersToCall = 'AddedUsersToCall', AddedUsersToCall = 'AddedUsersToCall',
AlreadyGroupMember = 'AlreadyGroupMember', AlreadyGroupMember = 'AlreadyGroupMember',
AlreadyRequestedToJoin = 'AlreadyRequestedToJoin', AlreadyRequestedToJoin = 'AlreadyRequestedToJoin',
AttachmentDownloadStillInProgress = 'AttachmentDownloadStillInProgress',
Blocked = 'Blocked', Blocked = 'Blocked',
BlockedGroup = 'BlockedGroup', BlockedGroup = 'BlockedGroup',
CallHistoryCleared = 'CallHistoryCleared', CallHistoryCleared = 'CallHistoryCleared',
@ -78,6 +79,10 @@ export type AnyToast =
} }
| { toastType: ToastType.AlreadyGroupMember } | { toastType: ToastType.AlreadyGroupMember }
| { toastType: ToastType.AlreadyRequestedToJoin } | { toastType: ToastType.AlreadyRequestedToJoin }
| {
toastType: ToastType.AttachmentDownloadStillInProgress;
parameters: { count: number };
}
| { toastType: ToastType.Blocked } | { toastType: ToastType.Blocked }
| { toastType: ToastType.BlockedGroup } | { toastType: ToastType.BlockedGroup }
| { toastType: ToastType.CallHistoryCleared } | { toastType: ToastType.CallHistoryCleared }
@ -108,7 +113,10 @@ export type AnyToast =
| { toastType: ToastType.FailedToFetchPhoneNumber } | { toastType: ToastType.FailedToFetchPhoneNumber }
| { toastType: ToastType.FailedToFetchUsername } | { toastType: ToastType.FailedToFetchUsername }
| { toastType: ToastType.FailedToSendWithEndorsements } | { toastType: ToastType.FailedToSendWithEndorsements }
| { toastType: ToastType.FileSaved; parameters: { fullPath: string } } | {
toastType: ToastType.FileSaved;
parameters: { fullPath: string; countOfFiles?: number };
}
| { | {
toastType: ToastType.FileSize; toastType: ToastType.FileSize;
parameters: { limit: number; units: string }; parameters: { limit: number; units: string };

View file

@ -231,15 +231,32 @@ async function writeWithAttributes(
export const saveAttachmentToDisk = async ({ export const saveAttachmentToDisk = async ({
data, data,
name, name,
baseDir,
}: { }: {
data: Uint8Array; data: Uint8Array;
name: string; name: string;
/**
* Base directory for saving the attachment.
* If omitted, a dialog will be opened to let the user choose a directory
*/
baseDir?: string;
}): Promise<null | { fullPath: string; name: string }> => { }): Promise<null | { fullPath: string; name: string }> => {
const { canceled, filePath } = await showSaveDialog(name); let filePath;
if (canceled || !filePath) { if (!baseDir) {
const { canceled, filePath: dialogFilePath } = await showSaveDialog(name);
if (canceled) {
return null; return null;
} }
if (!dialogFilePath) {
throw new Error(
"saveAttachmentToDisk: Dialog wasn't canceled, but returned path to attachment is null!"
);
}
filePath = dialogFilePath;
} else {
filePath = join(baseDir, name);
}
await writeWithAttributes(filePath, data); await writeWithAttributes(filePath, data);