Reintroduce file chooser dialog for every attachment save

This commit is contained in:
Scott Nonnenberg 2020-01-15 14:23:02 -08:00 committed by Ken Powers
parent 1c906e76f9
commit 55eff02872
5 changed files with 74 additions and 39 deletions

View file

@ -782,8 +782,8 @@
"message": "A voice message must have only one attachment.", "message": "A voice message must have only one attachment.",
"description": "Shown in toast if tries to record a voice note with any staged attachments" "description": "Shown in toast if tries to record a voice note with any staged attachments"
}, },
"attachmentSavedToDownloads": { "attachmentSaved": {
"message": "Attachment saved as \"$name$\" in your Downloads folder. Click to show.", "message": "Attachment saved. Click to show in folder.",
"description": "Shown after user selects to save to downloads", "description": "Shown after user selects to save to downloads",
"placeholders": { "placeholders": {
"name": { "name": {

View file

@ -1,6 +1,6 @@
const crypto = require('crypto'); const crypto = require('crypto');
const path = require('path'); const path = require('path');
const { app, shell, remote } = require('electron'); const { app, dialog, shell, remote } = require('electron');
const pify = require('pify'); const pify = require('pify');
const glob = require('glob'); const glob = require('glob');
@ -189,7 +189,16 @@ exports.writeToDownloads = async ({ data, name }) => {
throw new Error('Invalid filename!'); 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) { if (process.platform === 'darwin' && xattr) {
// kLSQuarantineTypeInstantMessageAttachment // kLSQuarantineTypeInstantMessageAttachment
@ -204,14 +213,9 @@ exports.writeToDownloads = async ({ data, name }) => {
// https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html // https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
const attrValue = `${type};${timestamp};${appName};${guid}`; 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 => { exports.openFileInDownloads = async name => {
const shellToUse = shell || remote.shell; const shellToUse = shell || remote.shell;
@ -229,6 +233,37 @@ exports.openFileInDownloads = async name => {
shellToUse.showItemInFolder(normalized); 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 -> // createWriterForNew :: AttachmentsPath ->
// ArrayBuffer -> // ArrayBuffer ->
// IO (Promise RelativePath) // IO (Promise RelativePath)

View file

@ -119,8 +119,8 @@ function initializeMigrations({
getPath, getPath,
getStickersPath, getStickersPath,
getTempPath, getTempPath,
openFileInDownloads, openFileInFolder,
writeToDownloads, saveAttachmentToDisk,
} = Attachments; } = Attachments;
const { const {
getImageDimensions, getImageDimensions,
@ -189,13 +189,13 @@ function initializeMigrations({
loadPreviewData, loadPreviewData,
loadQuoteData, loadQuoteData,
loadStickerData, loadStickerData,
openFileInDownloads, openFileInFolder,
readAttachmentData, readAttachmentData,
readDraftData, readDraftData,
readStickerData, readStickerData,
readTempData, readTempData,
run, run,
writeToDownloads, saveAttachmentToDisk,
processNewAttachment: attachment => processNewAttachment: attachment =>
MessageType.processNewAttachment(attachment, { MessageType.processNewAttachment(attachment, {
writeNewAttachmentData, writeNewAttachmentData,

View file

@ -19,18 +19,18 @@
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Message, MIME, VisualAttachment } = window.Signal.Types; const { Message, MIME, VisualAttachment } = window.Signal.Types;
const { const {
upgradeMessageSchema,
getAbsoluteAttachmentPath,
getAbsoluteDraftPath,
copyIntoTempDirectory, copyIntoTempDirectory,
getAbsoluteTempPath,
deleteDraftFile, deleteDraftFile,
deleteTempFile, deleteTempFile,
openFileInDownloads, getAbsoluteAttachmentPath,
getAbsoluteDraftPath,
getAbsoluteTempPath,
openFileInFolder,
readAttachmentData, readAttachmentData,
readDraftData, readDraftData,
saveAttachmentToDisk,
upgradeMessageSchema,
writeNewDraftData, writeNewDraftData,
writeToDownloads,
} = window.Signal.Migrations; } = window.Signal.Migrations;
const { const {
getOlderMessagesByConversation, getOlderMessagesByConversation,
@ -107,10 +107,10 @@
Whisper.FileSavedToast = Whisper.ToastView.extend({ Whisper.FileSavedToast = Whisper.ToastView.extend({
className: 'toast toast-clickable', className: 'toast toast-clickable',
initialize(options) { initialize(options) {
if (!options.name) { if (!options.fullPath) {
throw new Error('FileSavedToast: name option was not provided!'); throw new Error('FileSavedToast: name option was not provided!');
} }
this.name = options.name; this.fullPath = options.fullPath;
this.timeout = 10000; this.timeout = 10000;
if (window.getInteractionMode() === 'keyboard') { if (window.getInteractionMode() === 'keyboard') {
@ -124,7 +124,7 @@
keydown: 'onKeydown', keydown: 'onKeydown',
}, },
onClick() { onClick() {
openFileInDownloads(this.name); openFileInFolder(this.fullPath);
this.close(); this.close();
}, },
onKeydown(event) { onKeydown(event) {
@ -135,11 +135,11 @@
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
openFileInDownloads(this.name); openFileInFolder(this.fullPath);
this.close(); this.close();
}, },
render_attributes() { render_attributes() {
return { toastMessage: i18n('attachmentSavedToDownloads', this.name) }; return { toastMessage: i18n('attachmentSaved') };
}, },
}); });
@ -1796,13 +1796,13 @@
const saveAttachment = async ({ attachment, message } = {}) => { const saveAttachment = async ({ attachment, message } = {}) => {
const timestamp = message.sent_at; const timestamp = message.sent_at;
const name = await Signal.Types.Attachment.save({ const fullPath = await Signal.Types.Attachment.save({
attachment, attachment,
readAttachmentData, readAttachmentData,
writeToDownloads, saveAttachmentToDisk,
timestamp, timestamp,
}); });
this.showToast(Whisper.FileSavedToast, { name }); this.showToast(Whisper.FileSavedToast, { fullPath });
}; };
const onItemClick = async ({ message, attachment, type }) => { const onItemClick = async ({ message, attachment, type }) => {
@ -1993,13 +1993,13 @@
return; return;
} }
const name = await Signal.Types.Attachment.save({ const fullPath = await Signal.Types.Attachment.save({
attachment, attachment,
readAttachmentData, readAttachmentData,
writeToDownloads, saveAttachmentToDisk,
timestamp, timestamp,
}); });
this.showToast(Whisper.FileSavedToast, { name }); this.showToast(Whisper.FileSavedToast, { fullPath });
}, },
async displayTapToViewMessage(messageId) { async displayTapToViewMessage(messageId) {
@ -2196,14 +2196,14 @@
); );
const onSave = async (options = {}) => { const onSave = async (options = {}) => {
const name = await Signal.Types.Attachment.save({ const fullPath = await Signal.Types.Attachment.save({
attachment: options.attachment, attachment: options.attachment,
index: options.index + 1, index: options.index + 1,
readAttachmentData, readAttachmentData,
writeToDownloads, saveAttachmentToDisk,
timestamp: options.message.get('sent_at'), timestamp: options.message.get('sent_at'),
}); });
this.showToast(Whisper.FileSavedToast, { name }); this.showToast(Whisper.FileSavedToast, { fullPath });
}; };
const props = { const props = {

View file

@ -328,13 +328,13 @@ export const save = async ({
attachment, attachment,
index, index,
readAttachmentData, readAttachmentData,
writeToDownloads, saveAttachmentToDisk,
timestamp, timestamp,
}: { }: {
attachment: Attachment; attachment: Attachment;
index: number; index: number;
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>; readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
writeToDownloads: (options: { saveAttachmentToDisk: (options: {
data: ArrayBuffer; data: ArrayBuffer;
name: string; name: string;
}) => Promise<{ name: string; fullPath: string }>; }) => Promise<{ name: string; fullPath: string }>;
@ -349,12 +349,12 @@ export const save = async ({
: attachment.data; : attachment.data;
const name = getSuggestedFilename({ attachment, timestamp, index }); const name = getSuggestedFilename({ attachment, timestamp, index });
const { name: savedFilename } = await writeToDownloads({ const { fullPath } = await saveAttachmentToDisk({
data, data,
name, name,
}); });
return savedFilename; return fullPath;
}; };
export const getSuggestedFilename = ({ export const getSuggestedFilename = ({