diff --git a/app/main.main.ts b/app/main.main.ts index c9eaff594ed..d8e75ebca8c 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -3194,6 +3194,24 @@ ipc.on('show-item-in-folder', (_event, folder) => { shell.showItemInFolder(folder); }); +ipc.handle('open-file-path', async (_event, filePath: string) => { + if (!mainWindow) { + return; + } + const fileName = basename(filePath); + const { response } = await dialog.showMessageBox(mainWindow, { + type: 'question', + buttons: ['Open', 'Cancel'], + defaultId: 0, + cancelId: 1, + message: `Open "${fileName}"?`, + detail: 'This file will be opened with your default application.', + }); + if (response === 0) { + await shell.openPath(filePath); + } +}); + ipc.handle('show-save-dialog', async (_event, { defaultPath }) => { if (!mainWindow) { log.warn('show-save-dialog: no main window'); diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 3bd8bc83dba..a8837636db6 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -671,6 +671,10 @@ $message-padding-horizontal: 12px; cursor: pointer; } +.module-message__simple-attachment-container { + width: 100%; +} + .module-message__simple-attachment { @include mixins.button-reset; diff --git a/ts/components/conversation/Message.dom.tsx b/ts/components/conversation/Message.dom.tsx index d8f96e8e7a5..7c2ef5e74a7 100644 --- a/ts/components/conversation/Message.dom.tsx +++ b/ts/components/conversation/Message.dom.tsx @@ -19,6 +19,7 @@ import type { ConversationType, ConversationTypeType, InteractionModeType, + OpenAttachmentInDefaultAppActionCreatorType, PushPanelForConversationActionType, SaveAttachmentActionCreatorType, SaveAttachmentsActionCreatorType, @@ -386,6 +387,7 @@ export type PropsActions = { attachment: AttachmentType; messageId: string; }) => void; + openAttachmentInDefaultApp: OpenAttachmentInDefaultAppActionCreatorType; saveAttachment: SaveAttachmentActionCreatorType; saveAttachments: SaveAttachmentsActionCreatorType; showLightbox: (options: { @@ -1356,6 +1358,7 @@ export class Message extends React.PureComponent { // attachment. But we don't want the user to tab here unless that text exists. const tabIndex = text ? 0 : -1; return ( +
+ ); } @@ -3218,7 +3222,7 @@ export class Message extends React.PureComponent { const { id, attachments, - saveAttachment, + openAttachmentInDefaultApp, timestamp, kickOffAttachmentDownload, attachmentDroppedDueToSize, @@ -3251,7 +3255,7 @@ export class Message extends React.PureComponent { messageId: id, }); } else { - saveAttachment(firstAttachment, timestamp); + openAttachmentInDefaultApp(firstAttachment, timestamp); } }; diff --git a/ts/components/conversation/TimelineItem.dom.stories.tsx b/ts/components/conversation/TimelineItem.dom.stories.tsx index 69707c85b7f..150b3fbe293 100644 --- a/ts/components/conversation/TimelineItem.dom.stories.tsx +++ b/ts/components/conversation/TimelineItem.dom.stories.tsx @@ -73,6 +73,7 @@ const getDefaultProps = () => ({ messageExpanded: action('messageExpanded'), showConversation: action('showConversation'), openGiftBadge: action('openGiftBadge'), + openAttachmentInDefaultApp: action('openAttachmentInDefaultApp'), saveAttachment: action('saveAttachment'), saveAttachments: action('saveAttachments'), showPinMessageDialog: action('showPinMessageDialog'), diff --git a/ts/components/conversation/TimelineMessage.dom.stories.tsx b/ts/components/conversation/TimelineMessage.dom.stories.tsx index 658cbbe1cc7..a2b2da857f7 100644 --- a/ts/components/conversation/TimelineMessage.dom.stories.tsx +++ b/ts/components/conversation/TimelineMessage.dom.stories.tsx @@ -317,6 +317,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ : overrideProps.readStatus, renderReactionPicker, renderAudioAttachment, + openAttachmentInDefaultApp: action('openAttachmentInDefaultApp'), saveAttachment: action('saveAttachment'), saveAttachments: action('saveAttachments'), setQuoteByMessageId: action('setQuoteByMessageId'), diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index a15d49b3320..b096082902f 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -1,6 +1,7 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { tmpdir } from 'node:os'; import type { ThunkAction } from 'redux-thunk'; import lodash from 'lodash'; import { type PhoneNumber } from 'google-libphonenumber'; @@ -1227,6 +1228,7 @@ export const actions = { saveAttachment, saveAttachments, saveAttachmentFromMessage, + openAttachmentInDefaultApp, saveAvatarToDisk, scrollToMessage, scrollToOldestUnreadMention, @@ -4187,6 +4189,30 @@ function saveAttachment( }; } +export type OpenAttachmentInDefaultAppActionCreatorType = ReadonlyDeep< + (attachment: AttachmentType, timestamp?: number) => unknown +>; + +function openAttachmentInDefaultApp( + attachment: AttachmentType, + timestamp = Date.now() +): ThunkAction { + return async () => { + const fullPath = await Attachment.save({ + attachment, + getUnusedFilename, + readAttachmentData, + saveAttachmentToDisk, + timestamp, + baseDir: tmpdir(), + }); + + if (fullPath) { + await ipcRenderer.invoke('open-file-path', fullPath); + } + }; +} + const showSaveMultiDialog = ( i18n: LocalizerType ): Promise<{ diff --git a/ts/state/smart/TimelineItem.preload.tsx b/ts/state/smart/TimelineItem.preload.tsx index 879e07c6b73..bf9126ca46c 100644 --- a/ts/state/smart/TimelineItem.preload.tsx +++ b/ts/state/smart/TimelineItem.preload.tsx @@ -166,6 +166,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( markAttachmentAsCorrupted, messageExpanded, onPinnedMessageRemove, + openAttachmentInDefaultApp, openGiftBadge, retryDeleteForEveryone, retryMessageSend, @@ -297,6 +298,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( sendPollVote={sendPollVote} renderItem={renderItem} returnToActiveCall={returnToActiveCall} + openAttachmentInDefaultApp={openAttachmentInDefaultApp} saveAttachment={saveAttachment} saveAttachments={saveAttachments} scrollToPollMessage={scrollToPollMessage}