From 55eff02872a9c2b3b737263cb76b1bc1989200ec Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 15 Jan 2020 14:23:02 -0800 Subject: [PATCH] Reintroduce file chooser dialog for every attachment save --- _locales/en/messages.json | 4 +-- app/attachments.js | 53 +++++++++++++++++++++++++++++------ js/modules/signal.js | 8 +++--- js/views/conversation_view.js | 40 +++++++++++++------------- ts/types/Attachment.ts | 8 +++--- 5 files changed, 74 insertions(+), 39 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f9847a3d06..73de167a7c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -782,8 +782,8 @@ "message": "A voice message must have only one attachment.", "description": "Shown in toast if tries to record a voice note with any staged attachments" }, - "attachmentSavedToDownloads": { - "message": "Attachment saved as \"$name$\" in your Downloads folder. Click to show.", + "attachmentSaved": { + "message": "Attachment saved. Click to show in folder.", "description": "Shown after user selects to save to downloads", "placeholders": { "name": { diff --git a/app/attachments.js b/app/attachments.js index 7f573aa105..8c115058f4 100644 --- a/app/attachments.js +++ b/app/attachments.js @@ -1,6 +1,6 @@ const crypto = require('crypto'); const path = require('path'); -const { app, shell, remote } = require('electron'); +const { app, dialog, shell, remote } = require('electron'); const pify = require('pify'); const glob = require('glob'); @@ -189,7 +189,16 @@ exports.writeToDownloads = async ({ data, name }) => { throw new Error('Invalid filename!'); } - await fse.writeFile(normalized, Buffer.from(data)); + writeWithAttributes(normalized, Buffer.from(data)); + + return { + fullPath: normalized, + name: candidateName, + }; +}; + +async function writeWithAttributes(target, data) { + await fse.writeFile(target, Buffer.from(data)); if (process.platform === 'darwin' && xattr) { // kLSQuarantineTypeInstantMessageAttachment @@ -204,14 +213,9 @@ exports.writeToDownloads = async ({ data, name }) => { // https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html const attrValue = `${type};${timestamp};${appName};${guid}`; - await xattr.set(normalized, 'com.apple.quarantine', attrValue); + await xattr.set(target, 'com.apple.quarantine', attrValue); } - - return { - fullPath: normalized, - name: candidateName, - }; -}; +} exports.openFileInDownloads = async name => { const shellToUse = shell || remote.shell; @@ -229,6 +233,37 @@ exports.openFileInDownloads = async name => { shellToUse.showItemInFolder(normalized); }; +exports.saveAttachmentToDisk = async ({ data, name }) => { + const dialogToUse = dialog || remote.dialog; + const browserWindow = remote.getCurrentWindow(); + + const { canceled, filePath } = await dialogToUse.showSaveDialog( + browserWindow, + { + defaultPath: name, + } + ); + + if (canceled) { + return null; + } + + await writeWithAttributes(filePath, Buffer.from(data)); + + const basename = path.basename(filePath); + + return { + fullPath: filePath, + name: basename, + }; +}; + +exports.openFileInFolder = async target => { + const shellToUse = shell || remote.shell; + + shellToUse.showItemInFolder(target); +}; + // createWriterForNew :: AttachmentsPath -> // ArrayBuffer -> // IO (Promise RelativePath) diff --git a/js/modules/signal.js b/js/modules/signal.js index f6e15cf045..1ea07874e0 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -119,8 +119,8 @@ function initializeMigrations({ getPath, getStickersPath, getTempPath, - openFileInDownloads, - writeToDownloads, + openFileInFolder, + saveAttachmentToDisk, } = Attachments; const { getImageDimensions, @@ -189,13 +189,13 @@ function initializeMigrations({ loadPreviewData, loadQuoteData, loadStickerData, - openFileInDownloads, + openFileInFolder, readAttachmentData, readDraftData, readStickerData, readTempData, run, - writeToDownloads, + saveAttachmentToDisk, processNewAttachment: attachment => MessageType.processNewAttachment(attachment, { writeNewAttachmentData, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 00bdedcd79..029260d11b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -19,18 +19,18 @@ window.Whisper = window.Whisper || {}; const { Message, MIME, VisualAttachment } = window.Signal.Types; const { - upgradeMessageSchema, - getAbsoluteAttachmentPath, - getAbsoluteDraftPath, copyIntoTempDirectory, - getAbsoluteTempPath, deleteDraftFile, deleteTempFile, - openFileInDownloads, + getAbsoluteAttachmentPath, + getAbsoluteDraftPath, + getAbsoluteTempPath, + openFileInFolder, readAttachmentData, readDraftData, + saveAttachmentToDisk, + upgradeMessageSchema, writeNewDraftData, - writeToDownloads, } = window.Signal.Migrations; const { getOlderMessagesByConversation, @@ -107,10 +107,10 @@ Whisper.FileSavedToast = Whisper.ToastView.extend({ className: 'toast toast-clickable', initialize(options) { - if (!options.name) { + if (!options.fullPath) { throw new Error('FileSavedToast: name option was not provided!'); } - this.name = options.name; + this.fullPath = options.fullPath; this.timeout = 10000; if (window.getInteractionMode() === 'keyboard') { @@ -124,7 +124,7 @@ keydown: 'onKeydown', }, onClick() { - openFileInDownloads(this.name); + openFileInFolder(this.fullPath); this.close(); }, onKeydown(event) { @@ -135,11 +135,11 @@ event.preventDefault(); event.stopPropagation(); - openFileInDownloads(this.name); + openFileInFolder(this.fullPath); this.close(); }, render_attributes() { - return { toastMessage: i18n('attachmentSavedToDownloads', this.name) }; + return { toastMessage: i18n('attachmentSaved') }; }, }); @@ -1796,13 +1796,13 @@ const saveAttachment = async ({ attachment, message } = {}) => { const timestamp = message.sent_at; - const name = await Signal.Types.Attachment.save({ + const fullPath = await Signal.Types.Attachment.save({ attachment, readAttachmentData, - writeToDownloads, + saveAttachmentToDisk, timestamp, }); - this.showToast(Whisper.FileSavedToast, { name }); + this.showToast(Whisper.FileSavedToast, { fullPath }); }; const onItemClick = async ({ message, attachment, type }) => { @@ -1993,13 +1993,13 @@ return; } - const name = await Signal.Types.Attachment.save({ + const fullPath = await Signal.Types.Attachment.save({ attachment, readAttachmentData, - writeToDownloads, + saveAttachmentToDisk, timestamp, }); - this.showToast(Whisper.FileSavedToast, { name }); + this.showToast(Whisper.FileSavedToast, { fullPath }); }, async displayTapToViewMessage(messageId) { @@ -2196,14 +2196,14 @@ ); const onSave = async (options = {}) => { - const name = await Signal.Types.Attachment.save({ + const fullPath = await Signal.Types.Attachment.save({ attachment: options.attachment, index: options.index + 1, readAttachmentData, - writeToDownloads, + saveAttachmentToDisk, timestamp: options.message.get('sent_at'), }); - this.showToast(Whisper.FileSavedToast, { name }); + this.showToast(Whisper.FileSavedToast, { fullPath }); }; const props = { diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 7b86f6021e..0d3e532af8 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -328,13 +328,13 @@ export const save = async ({ attachment, index, readAttachmentData, - writeToDownloads, + saveAttachmentToDisk, timestamp, }: { attachment: Attachment; index: number; readAttachmentData: (relativePath: string) => Promise; - writeToDownloads: (options: { + saveAttachmentToDisk: (options: { data: ArrayBuffer; name: string; }) => Promise<{ name: string; fullPath: string }>; @@ -349,12 +349,12 @@ export const save = async ({ : attachment.data; const name = getSuggestedFilename({ attachment, timestamp, index }); - const { name: savedFilename } = await writeToDownloads({ + const { fullPath } = await saveAttachmentToDisk({ data, name, }); - return savedFilename; + return fullPath; }; export const getSuggestedFilename = ({