mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 05:04:58 +08:00
Add a context menu to attachment items with the direct-open / save options #1548
This commit is contained in:
parent
020780223d
commit
9d1b8fe600
5 changed files with 90 additions and 71 deletions
|
@ -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
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue