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": {
|
"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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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."
|
||||||
|
|
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 };
|
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'];
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
),
|
),
|
||||||
|
|
|
@ -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'
|
||||||
),
|
),
|
||||||
|
|
|
@ -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'
|
||||||
),
|
),
|
||||||
|
|
|
@ -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'
|
||||||
),
|
),
|
||||||
|
|
|
@ -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;
|
||||||
if (!isDownloaded(attachment)) {
|
// check if any attachment needs to be downloaded from servers
|
||||||
kickOffAttachmentDownload({
|
for (const attachment of attachments) {
|
||||||
attachment,
|
if (!isDownloaded(attachment)) {
|
||||||
messageId: id,
|
kickOffAttachmentDownload({
|
||||||
});
|
attachment,
|
||||||
return;
|
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);
|
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;
|
||||||
|
|
||||||
|
|
|
@ -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: (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -231,14 +231,31 @@ 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) {
|
||||||
return null;
|
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);
|
await writeWithAttributes(filePath, data);
|
||||||
|
|
Loading…
Reference in a new issue