Allow downloading multiple images into one directory
Co-authored-by: Major-Mayer <lrdarknesss@yahoo.de>
This commit is contained in:
parent
35946ef53c
commit
76e2597d30
21 changed files with 282 additions and 24 deletions
|
@ -791,9 +791,15 @@
|
|||
"icu:attachmentSaved": {
|
||||
"messageformat": "Anhang gespeichert."
|
||||
},
|
||||
"icu:attachmentSavedPlural": {
|
||||
"messageformat": "{count, plural, one {Anhang} other {# Anhänge}} gespeichert."
|
||||
},
|
||||
"icu:attachmentSavedShow": {
|
||||
"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": {
|
||||
"messageformat": "Du"
|
||||
},
|
||||
|
|
|
@ -1066,10 +1066,18 @@
|
|||
"messageformat": "Attachment saved.",
|
||||
"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": {
|
||||
"messageformat": "Show in folder",
|
||||
"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": {
|
||||
"messageformat": "You",
|
||||
"description": "Shown when the user represented is the current user."
|
||||
|
|
26
app/main.ts
26
app/main.ts
|
@ -3032,6 +3032,32 @@ ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
|
|||
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) => {
|
||||
const role = untypedRole as MenuItemConstructorOptions['role'];
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
renderAudioAttachment: () => <div />,
|
||||
renderingContext: 'EditHistoryMessagesModal',
|
||||
saveAttachment: shouldNeverBeCalled,
|
||||
saveAttachments: shouldNeverBeCalled,
|
||||
scrollToQuotedMessage: shouldNeverBeCalled,
|
||||
shouldCollapseAbove: false,
|
||||
shouldCollapseBelow: false,
|
||||
|
@ -62,6 +63,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
showContactModal: shouldNeverBeCalled,
|
||||
showConversation: noop,
|
||||
showEditHistoryModal: noop,
|
||||
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
|
||||
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
|
||||
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
|
||||
showLightboxForViewOnceMedia: shouldNeverBeCalled,
|
||||
|
|
|
@ -64,8 +64,10 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
pushPanelForConversation: shouldNeverBeCalled,
|
||||
renderAudioAttachment: () => <div />,
|
||||
saveAttachment: shouldNeverBeCalled,
|
||||
saveAttachments: shouldNeverBeCalled,
|
||||
scrollToQuotedMessage: shouldNeverBeCalled,
|
||||
showConversation: noop,
|
||||
showAttachmentDownloadStillInProgressToast: shouldNeverBeCalled,
|
||||
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
|
||||
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
|
||||
showLightbox: shouldNeverBeCalled,
|
||||
|
|
|
@ -27,6 +27,13 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.AlreadyGroupMember };
|
||||
case ToastType.AlreadyRequestedToJoin:
|
||||
return { toastType: ToastType.AlreadyRequestedToJoin };
|
||||
case ToastType.AttachmentDownloadStillInProgress:
|
||||
return {
|
||||
toastType: ToastType.AttachmentDownloadStillInProgress,
|
||||
parameters: {
|
||||
count: 1,
|
||||
},
|
||||
};
|
||||
case ToastType.Blocked:
|
||||
return { toastType: ToastType.Blocked };
|
||||
case ToastType.BlockedGroup:
|
||||
|
|
|
@ -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) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import type {
|
|||
InteractionModeType,
|
||||
PushPanelForConversationActionType,
|
||||
SaveAttachmentActionCreatorType,
|
||||
SaveAttachmentsActionCreatorType,
|
||||
ShowConversationType,
|
||||
} from '../../state/ducks/conversations';
|
||||
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
|
||||
|
@ -352,6 +353,7 @@ export type PropsActions = {
|
|||
messageId: string;
|
||||
}) => void;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
saveAttachments: SaveAttachmentsActionCreatorType;
|
||||
showLightbox: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -366,6 +368,7 @@ export type PropsActions = {
|
|||
targetMessage?: (messageId: string, conversationId: string) => unknown;
|
||||
|
||||
showEditHistoryModal?: (id: string) => unknown;
|
||||
showAttachmentDownloadStillInProgressToast: (count: number) => unknown;
|
||||
showExpiredIncomingTapToViewToast: () => unknown;
|
||||
showExpiredOutgoingTapToViewToast: () => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
|
|
|
@ -95,9 +95,11 @@ export type PropsReduxActions = Pick<
|
|||
| 'pushPanelForConversation'
|
||||
| 'retryMessageSend'
|
||||
| 'saveAttachment'
|
||||
| 'saveAttachments'
|
||||
| 'showContactModal'
|
||||
| 'showConversation'
|
||||
| 'showEditHistoryModal'
|
||||
| 'showAttachmentDownloadStillInProgressToast'
|
||||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showLightbox'
|
||||
|
@ -139,9 +141,11 @@ export function MessageDetail({
|
|||
retryMessageSend,
|
||||
renderAudioAttachment,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
showContactModal,
|
||||
showConversation,
|
||||
showEditHistoryModal,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showLightbox,
|
||||
|
@ -348,6 +352,7 @@ export function MessageDetail({
|
|||
retryMessageSend={retryMessageSend}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAttachments={saveAttachments}
|
||||
shouldCollapseAbove={false}
|
||||
shouldCollapseBelow={false}
|
||||
shouldHideMetadata={false}
|
||||
|
@ -357,6 +362,9 @@ export function MessageDetail({
|
|||
log.warn('MessageDetail: scrollToQuotedMessage called!');
|
||||
}}
|
||||
showContactModal={showContactModal}
|
||||
showAttachmentDownloadStillInProgressToast={
|
||||
showAttachmentDownloadStillInProgressToast
|
||||
}
|
||||
showExpiredIncomingTapToViewToast={
|
||||
showExpiredIncomingTapToViewToast
|
||||
}
|
||||
|
|
|
@ -125,6 +125,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
copyMessageText: action('copyMessageText'),
|
||||
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
saveAttachments: action('saveAttachments'),
|
||||
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||
targetMessage: action('default--targetMessage'),
|
||||
shouldCollapseAbove: false,
|
||||
|
@ -133,6 +134,9 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
showSpoiler: action('showSpoiler'),
|
||||
pushPanelForConversation: action('default--pushPanelForConversation'),
|
||||
showContactModal: action('default--showContactModal'),
|
||||
showAttachmentDownloadStillInProgressToast: action(
|
||||
'showAttachmentDownloadStillInProgressToast'
|
||||
),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
|
|
|
@ -291,6 +291,7 @@ const actions = () => ({
|
|||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
saveAttachments: action('saveAttachments'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
|
@ -305,6 +306,9 @@ const actions = () => ({
|
|||
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
showAttachmentDownloadStillInProgressToast: action(
|
||||
'showAttachmentDownloadStillInProgressToast'
|
||||
),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
|
|
|
@ -82,6 +82,7 @@ const getDefaultProps = () => ({
|
|||
showConversation: action('showConversation'),
|
||||
openGiftBadge: action('openGiftBadge'),
|
||||
saveAttachment: action('saveAttachment'),
|
||||
saveAttachments: action('saveAttachments'),
|
||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||
onOutgoingAudioCallInConversation: action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
|
@ -96,6 +97,9 @@ const getDefaultProps = () => ({
|
|||
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
|
||||
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
showAttachmentDownloadStillInProgressToast: action(
|
||||
'showAttachmentDownloadStillInProgressToast'
|
||||
),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
|
|
|
@ -317,6 +317,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
renderReactionPicker,
|
||||
renderAudioAttachment,
|
||||
saveAttachment: action('saveAttachment'),
|
||||
saveAttachments: action('saveAttachments'),
|
||||
setQuoteByMessageId: action('setQuoteByMessageId'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
copyMessageText: action('copyMessageText'),
|
||||
|
@ -340,6 +341,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
showSpoiler: action('showSpoiler'),
|
||||
pushPanelForConversation: action('pushPanelForConversation'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showAttachmentDownloadStillInProgressToast: action(
|
||||
'showAttachmentDownloadStillInProgressToast'
|
||||
),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
|
|
|
@ -121,6 +121,8 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
selectedReaction,
|
||||
setQuoteByMessageId,
|
||||
setMessageToEdit,
|
||||
|
@ -212,22 +214,42 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!attachments || attachments.length !== 1) {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
return;
|
||||
let attachmentsInProgress = 0;
|
||||
// check if any attachment needs to be downloaded from servers
|
||||
for (const attachment of attachments) {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
});
|
||||
|
||||
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);
|
||||
|
@ -237,16 +259,13 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
const shouldShowAdditional =
|
||||
doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow;
|
||||
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
const firstAttachment = attachments && attachments[0];
|
||||
const hasPendingAttachments =
|
||||
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 =
|
||||
canDownload &&
|
||||
!isSticker &&
|
||||
!multipleAttachments &&
|
||||
!isTapToView &&
|
||||
firstAttachment &&
|
||||
!firstAttachment.pending
|
||||
canDownload && !isSticker && !isTapToView && !hasPendingAttachments
|
||||
? openGenericAttachment
|
||||
: undefined;
|
||||
|
||||
|
|
|
@ -115,6 +115,7 @@ type MigrationsModuleType = {
|
|||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
baseDir?: string;
|
||||
}) => Promise<null | { fullPath: string; name: string }>;
|
||||
processNewAttachment: (attachment: AttachmentType) => Promise<AttachmentType>;
|
||||
processNewSticker: (stickerData: Uint8Array) => Promise<
|
||||
|
@ -406,9 +407,11 @@ type AttachmentsModuleType = {
|
|||
saveAttachmentToDisk: ({
|
||||
data,
|
||||
name,
|
||||
dirName,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
dirName?: string;
|
||||
}) => Promise<null | { fullPath: string; name: string }>;
|
||||
|
||||
ensureAttachmentIsReencryptable: (
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from 'lodash';
|
||||
import type { PhoneNumber } from 'google-libphonenumber';
|
||||
|
||||
import { clipboard } from 'electron';
|
||||
import { clipboard, ipcRenderer } from 'electron';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
|
@ -1163,6 +1163,7 @@ export const actions = {
|
|||
reviewConversationNameCollision,
|
||||
revokePendingMembershipsFromGroupV2,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
saveAttachmentFromMessage,
|
||||
saveAvatarToDisk,
|
||||
scrollToMessage,
|
||||
|
@ -1189,6 +1190,7 @@ export const actions = {
|
|||
setPreJoinConversation,
|
||||
setVoiceNotePlaybackRate,
|
||||
showArchivedConversations,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showChooseGroupMembers,
|
||||
showConversation,
|
||||
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(
|
||||
messageId: string,
|
||||
providedAttachment?: AttachmentType
|
||||
|
|
|
@ -49,7 +49,9 @@ export const SmartMessageDetail = memo(
|
|||
popPanelForConversation,
|
||||
pushPanelForConversation,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
showConversation,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showSpoiler,
|
||||
|
@ -99,10 +101,14 @@ export const SmartMessageDetail = memo(
|
|||
receivedAt={receivedAt}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAttachments={saveAttachments}
|
||||
sentAt={message.timestamp}
|
||||
showContactModal={showContactModal}
|
||||
showConversation={showConversation}
|
||||
showEditHistoryModal={showEditHistoryModal}
|
||||
showAttachmentDownloadStillInProgressToast={
|
||||
showAttachmentDownloadStillInProgressToast
|
||||
}
|
||||
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
|
||||
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
|
||||
showLightbox={showLightbox}
|
||||
|
|
|
@ -126,10 +126,12 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
retryDeleteForEveryone,
|
||||
retryMessageSend,
|
||||
saveAttachment,
|
||||
saveAttachments,
|
||||
targetMessage,
|
||||
toggleSelectMessage,
|
||||
setMessageToEdit,
|
||||
showConversation,
|
||||
showAttachmentDownloadStillInProgressToast,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showSpoiler,
|
||||
|
@ -218,12 +220,16 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||
retryMessageSend={retryMessageSend}
|
||||
returnToActiveCall={returnToActiveCall}
|
||||
saveAttachment={saveAttachment}
|
||||
saveAttachments={saveAttachments}
|
||||
scrollToQuotedMessage={scrollToQuotedMessage}
|
||||
targetMessage={targetMessage}
|
||||
setQuoteByMessageId={setQuoteByMessageId}
|
||||
setMessageToEdit={setMessageToEdit}
|
||||
showContactModal={showContactModal}
|
||||
showConversation={showConversation}
|
||||
showAttachmentDownloadStillInProgressToast={
|
||||
showAttachmentDownloadStillInProgressToast
|
||||
}
|
||||
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
|
||||
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
|
||||
showLightbox={showLightbox}
|
||||
|
|
|
@ -983,6 +983,7 @@ export const save = async ({
|
|||
readAttachmentData,
|
||||
saveAttachmentToDisk,
|
||||
timestamp,
|
||||
baseDir,
|
||||
}: {
|
||||
attachment: AttachmentType;
|
||||
index?: number;
|
||||
|
@ -992,8 +993,14 @@ export const save = async ({
|
|||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
baseDir?: string;
|
||||
}) => Promise<{ name: string; fullPath: string } | null>;
|
||||
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> => {
|
||||
let data: Uint8Array;
|
||||
if (attachment.path) {
|
||||
|
@ -1009,6 +1016,7 @@ export const save = async ({
|
|||
const result = await saveAttachmentToDisk({
|
||||
data,
|
||||
name,
|
||||
baseDir,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
|
|
|
@ -6,6 +6,7 @@ export enum ToastType {
|
|||
AddedUsersToCall = 'AddedUsersToCall',
|
||||
AlreadyGroupMember = 'AlreadyGroupMember',
|
||||
AlreadyRequestedToJoin = 'AlreadyRequestedToJoin',
|
||||
AttachmentDownloadStillInProgress = 'AttachmentDownloadStillInProgress',
|
||||
Blocked = 'Blocked',
|
||||
BlockedGroup = 'BlockedGroup',
|
||||
CallHistoryCleared = 'CallHistoryCleared',
|
||||
|
@ -78,6 +79,10 @@ export type AnyToast =
|
|||
}
|
||||
| { toastType: ToastType.AlreadyGroupMember }
|
||||
| { toastType: ToastType.AlreadyRequestedToJoin }
|
||||
| {
|
||||
toastType: ToastType.AttachmentDownloadStillInProgress;
|
||||
parameters: { count: number };
|
||||
}
|
||||
| { toastType: ToastType.Blocked }
|
||||
| { toastType: ToastType.BlockedGroup }
|
||||
| { toastType: ToastType.CallHistoryCleared }
|
||||
|
@ -108,7 +113,10 @@ export type AnyToast =
|
|||
| { toastType: ToastType.FailedToFetchPhoneNumber }
|
||||
| { toastType: ToastType.FailedToFetchUsername }
|
||||
| { toastType: ToastType.FailedToSendWithEndorsements }
|
||||
| { toastType: ToastType.FileSaved; parameters: { fullPath: string } }
|
||||
| {
|
||||
toastType: ToastType.FileSaved;
|
||||
parameters: { fullPath: string; countOfFiles?: number };
|
||||
}
|
||||
| {
|
||||
toastType: ToastType.FileSize;
|
||||
parameters: { limit: number; units: string };
|
||||
|
|
|
@ -231,14 +231,31 @@ async function writeWithAttributes(
|
|||
export const saveAttachmentToDisk = async ({
|
||||
data,
|
||||
name,
|
||||
baseDir,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
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 }> => {
|
||||
const { canceled, filePath } = await showSaveDialog(name);
|
||||
let filePath;
|
||||
|
||||
if (canceled || !filePath) {
|
||||
return null;
|
||||
if (!baseDir) {
|
||||
const { canceled, filePath: dialogFilePath } = await showSaveDialog(name);
|
||||
if (canceled) {
|
||||
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);
|
||||
|
|
Loading…
Reference in a new issue