Add a context menu to attachment items with the direct-open / save options #1548

This commit is contained in:
Ben Gotow 2019-06-30 20:37:36 -05:00
parent 020780223d
commit 9d1b8fe600
5 changed files with 90 additions and 71 deletions

View file

@ -31,26 +31,6 @@ class MessageAttachments extends Component<MessageAttachmentsProps> {
filePreviewPaths: {}, filePreviewPaths: {},
}; };
onOpenAttachment = file => {
Actions.fetchAndOpenFile(file);
};
onRemoveAttachment = file => {
const { headerMessageId } = this.props;
Actions.removeAttachment({
headerMessageId: headerMessageId,
file: file,
});
};
onDownloadAttachment = file => {
Actions.fetchAndSaveFile(file);
};
onAbortDownload = file => {
Actions.abortFetchFile(file);
};
renderAttachment(AttachmentRenderer, file) { renderAttachment(AttachmentRenderer, file) {
const { canRemoveAttachments, downloads, filePreviewPaths } = this.props; const { canRemoveAttachments, downloads, filePreviewPaths } = this.props;
const download = downloads[file.id]; const download = downloads[file.id];
@ -73,10 +53,13 @@ class MessageAttachments extends Component<MessageAttachmentsProps> {
displaySize={displaySize} displaySize={displaySize}
fileIconName={fileIconName} fileIconName={fileIconName}
filePreviewPath={filePreviewPath} filePreviewPath={filePreviewPath}
onOpenAttachment={() => this.onOpenAttachment(file)} onOpenAttachment={() => Actions.fetchAndOpenFile(file)}
onDownloadAttachment={() => this.onDownloadAttachment(file)} onSaveAttachment={() => Actions.fetchAndSaveFile(file)}
onAbortDownload={() => this.onAbortDownload(file)} onRemoveAttachment={
onRemoveAttachment={canRemoveAttachments ? () => this.onRemoveAttachment(file) : null} canRemoveAttachments
? () => Actions.removeAttachment({ headerMessageId: this.props.headerMessageId, file })
: null
}
/> />
); );
} }

View file

@ -9,6 +9,7 @@ import { pickHTMLProps } from 'pick-react-known-prop';
import { RetinaImg } from './retina-img'; import { RetinaImg } from './retina-img';
import { Flexbox } from './flexbox'; import { Flexbox } from './flexbox';
import { Spinner } from './spinner'; import { Spinner } from './spinner';
import { localized } from '../intl';
const propTypes = { const propTypes = {
className: PropTypes.string, className: PropTypes.string,
@ -26,8 +27,7 @@ const propTypes = {
filePreviewPath: PropTypes.string, filePreviewPath: PropTypes.string,
onOpenAttachment: PropTypes.func, onOpenAttachment: PropTypes.func,
onRemoveAttachment: PropTypes.func, onRemoveAttachment: PropTypes.func,
onDownloadAttachment: PropTypes.func, onSaveAttachment: PropTypes.func,
onAbortDownload: PropTypes.func,
}; };
const defaultProps = { const defaultProps = {
@ -36,8 +36,47 @@ const defaultProps = {
const SPACE = ' '; const SPACE = ' ';
function ProgressBar(props) { function buildContextMenu(fns: {
const { download } = props; onOpenAttachment?: () => void;
onPreviewAttachment?: () => void;
onRemoveAttachment?: () => void;
onSaveAttachment?: () => void;
}) {
const { remote } = require('electron');
const template: Electron.MenuItemConstructorOptions[] = [];
if (fns.onOpenAttachment) {
template.push({
click: () => fns.onOpenAttachment(),
label: localized('Open'),
});
}
if (fns.onRemoveAttachment) {
template.push({
click: () => fns.onRemoveAttachment(),
label: localized('Remove'),
});
}
if (fns.onPreviewAttachment) {
template.push({
click: () => fns.onPreviewAttachment(),
label: localized('Preview'),
});
}
if (fns.onSaveAttachment) {
template.push({
click: () => fns.onSaveAttachment(),
label: localized('Save Into...'),
});
}
remote.Menu.buildFromTemplate(template).popup({});
}
const ProgressBar: React.FunctionComponent<{
download: {
state: string;
percent: number;
};
}> = ({ download }) => {
const isDownloading = download ? download.state === 'downloading' : false; const isDownloading = download ? download.state === 'downloading' : false;
if (!isDownloading) { if (!isDownloading) {
return <span />; return <span />;
@ -52,8 +91,7 @@ function ProgressBar(props) {
<span className="progress-foreground" style={downloadProgressStyle} /> <span className="progress-foreground" style={downloadProgressStyle} />
</span> </span>
); );
} };
ProgressBar.propTypes = propTypes;
function AttachmentActionIcon(props) { function AttachmentActionIcon(props) {
const { const {
@ -61,9 +99,8 @@ function AttachmentActionIcon(props) {
removeIcon, removeIcon,
downloadIcon, downloadIcon,
retinaImgMode, retinaImgMode,
onAbortDownload,
onRemoveAttachment, onRemoveAttachment,
onDownloadAttachment, onSaveAttachment,
} = props; } = props;
const isRemovable = onRemoveAttachment != null; const isRemovable = onRemoveAttachment != null;
@ -74,10 +111,8 @@ function AttachmentActionIcon(props) {
event.stopPropagation(); // Prevent 'onOpenAttachment' event.stopPropagation(); // Prevent 'onOpenAttachment'
if (isRemovable) { if (isRemovable) {
onRemoveAttachment(); onRemoveAttachment();
} else if (isDownloading && onAbortDownload != null) { } else if (onSaveAttachment != null) {
onAbortDownload(); onSaveAttachment();
} else if (onDownloadAttachment != null) {
onDownloadAttachment();
} }
}; };
@ -109,8 +144,8 @@ interface AttachmentItemProps {
fileIconName?: string; fileIconName?: string;
filePreviewPath?: string; filePreviewPath?: string;
onOpenAttachment?: () => void; onOpenAttachment?: () => void;
onSaveAttachment?: () => void;
onRemoveAttachment: () => void; onRemoveAttachment: () => void;
onDownloadAttachment?: () => void;
} }
export class AttachmentItem extends Component<AttachmentItemProps> { export class AttachmentItem extends Component<AttachmentItemProps> {
@ -142,13 +177,6 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
} }
}; };
_onOpenAttachment = () => {
const { onOpenAttachment } = this.props;
if (onOpenAttachment != null) {
onOpenAttachment();
}
};
_onAttachmentKeyDown = event => { _onAttachmentKeyDown = event => {
if (event.key === SPACE && this.props.filePreviewPath) { if (event.key === SPACE && this.props.filePreviewPath) {
event.preventDefault(); event.preventDefault();
@ -163,9 +191,11 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
} }
}; };
_onClickQuicklookIcon = event => { _onClickQuicklookIcon = (event?: React.MouseEvent<any>) => {
event.preventDefault(); if (event) {
event.stopPropagation(); event.preventDefault();
event.stopPropagation();
}
Actions.quickPreviewFile(this.props.filePath); Actions.quickPreviewFile(this.props.filePath);
}; };
@ -179,6 +209,8 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
displaySize, displaySize,
fileIconName, fileIconName,
filePreviewPath, filePreviewPath,
onOpenAttachment,
onSaveAttachment,
...extraProps ...extraProps
} = this.props; } = this.props;
const classes = classnames({ const classes = classnames({
@ -197,8 +229,15 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
tabIndex={tabIndex} tabIndex={tabIndex}
onKeyDown={focusable ? this._onAttachmentKeyDown : null} onKeyDown={focusable ? this._onAttachmentKeyDown : null}
draggable={draggable} draggable={draggable}
onDoubleClick={this._onOpenAttachment} onDoubleClick={onOpenAttachment}
onDragStart={this._onDragStart} onDragStart={this._onDragStart}
onContextMenu={() =>
buildContextMenu({
onPreviewAttachment: this._onClickQuicklookIcon,
onOpenAttachment,
onSaveAttachment,
})
}
{...pickHTMLProps(extraProps)} {...pickHTMLProps(extraProps)}
> >
{filePreviewPath ? ( {filePreviewPath ? (
@ -228,11 +267,11 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
</span> </span>
<span className="file-size">{displaySize ? `(${displaySize})` : ''}</span> <span className="file-size">{displaySize ? `(${displaySize})` : ''}</span>
</div> </div>
{filePreviewPath ? ( {filePreviewPath && (
<div className="file-action-icon quicklook" onClick={this._onClickQuicklookIcon}> <div className="file-action-icon quicklook" onClick={this._onClickQuicklookIcon}>
<RetinaImg name="attachment-quicklook.png" mode={RetinaImg.Mode.ContentIsMask} /> <RetinaImg name="attachment-quicklook.png" mode={RetinaImg.Mode.ContentIsMask} />
</div> </div>
) : null} )}
<AttachmentActionIcon <AttachmentActionIcon
{...this.props} {...this.props}
removeIcon="remove-attachment.png" removeIcon="remove-attachment.png"
@ -258,13 +297,6 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
static containerRequired = false; static containerRequired = false;
_onOpenAttachment = () => {
const { onOpenAttachment } = this.props;
if (onOpenAttachment != null) {
onOpenAttachment();
}
};
_onImgLoaded = () => { _onImgLoaded = () => {
// on load, modify our DOM just /slightly/. This causes DOM mutation listeners // on load, modify our DOM just /slightly/. This causes DOM mutation listeners
// watching the DOM to trigger. This is a good thing, because the image may // watching the DOM to trigger. This is a good thing, because the image may
@ -301,10 +333,19 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
} }
render() { render() {
const { className, displayName, download, ...extraProps } = this.props; const {
const classes = `nylas-attachment-item image-attachment-item ${className || ''}`; className,
displayName,
download,
onOpenAttachment,
onSaveAttachment,
...extraProps
} = this.props;
return ( return (
<div className={classes} {...pickHTMLProps(extraProps)}> <div
className={`nylas-attachment-item image-attachment-item ${className || ''}`}
{...pickHTMLProps(extraProps)}
>
<div> <div>
<ProgressBar download={download} /> <ProgressBar download={download} />
<AttachmentActionIcon <AttachmentActionIcon
@ -312,9 +353,12 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
removeIcon="image-cancel-button.png" removeIcon="image-cancel-button.png"
downloadIcon="image-download-button.png" downloadIcon="image-download-button.png"
retinaImgMode={RetinaImg.Mode.ContentPreserve} retinaImgMode={RetinaImg.Mode.ContentPreserve}
onAbortDownload={null}
/> />
<div className="file-preview" onDoubleClick={this._onOpenAttachment}> <div
className="file-preview"
onDoubleClick={onOpenAttachment}
onContextMenu={() => buildContextMenu({ onOpenAttachment, onSaveAttachment })}
>
<div className="file-name-container"> <div className="file-name-container">
<div className="file-name" title={displayName}> <div className="file-name" title={displayName}>
{displayName} {displayName}

View file

@ -463,7 +463,6 @@ export const fetchAndOpenFile = create('fetchAndOpenFile', ActionScopeWindow);
export const fetchAndSaveFile = create('fetchAndSaveFile', ActionScopeWindow); export const fetchAndSaveFile = create('fetchAndSaveFile', ActionScopeWindow);
export const fetchAndSaveAllFiles = create('fetchAndSaveAllFiles', ActionScopeWindow); export const fetchAndSaveAllFiles = create('fetchAndSaveAllFiles', ActionScopeWindow);
export const fetchFile = create('fetchFile', ActionScopeWindow); export const fetchFile = create('fetchFile', ActionScopeWindow);
export const abortFetchFile = create('abortFetchFile', ActionScopeWindow);
export const quickPreviewFile = create('quickPreviewFile', ActionScopeWindow); export const quickPreviewFile = create('quickPreviewFile', ActionScopeWindow);
/* /*

View file

@ -42,7 +42,6 @@ class AttachmentStore extends MailspringStore {
this.listenTo(Actions.fetchAndOpenFile, this._fetchAndOpen); this.listenTo(Actions.fetchAndOpenFile, this._fetchAndOpen);
this.listenTo(Actions.fetchAndSaveFile, this._fetchAndSave); this.listenTo(Actions.fetchAndSaveFile, this._fetchAndSave);
this.listenTo(Actions.fetchAndSaveAllFiles, this._fetchAndSaveAll); this.listenTo(Actions.fetchAndSaveAllFiles, this._fetchAndSaveAll);
this.listenTo(Actions.abortFetchFile, this._abortFetchFile);
this.listenTo(Actions.quickPreviewFile, this._quickPreviewFile); this.listenTo(Actions.quickPreviewFile, this._quickPreviewFile);
// sending // sending
@ -260,12 +259,6 @@ class AttachmentStore extends MailspringStore {
}); });
}; };
_abortFetchFile = () => {
// file
// put this back if we ever support downloading individual files again
return;
};
_defaultSaveDir() { _defaultSaveDir() {
let home = ''; let home = '';
if (process.platform === 'win32') { if (process.platform === 'win32') {

View file

@ -7,7 +7,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"lib": ["es2017", "dom"], "lib": ["es2017", "dom", "esnext.array"],
"removeComments": true, "removeComments": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"inlineSourceMap": true, "inlineSourceMap": true,