mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-24 08:04:11 +08:00
346 lines
9.8 KiB
TypeScript
346 lines
9.8 KiB
TypeScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
Thread,
|
|
Message,
|
|
localized,
|
|
Utils,
|
|
Actions,
|
|
AttachmentStore,
|
|
MessageStore,
|
|
} from 'mailspring-exports';
|
|
import { RetinaImg, InjectedComponentSet, InjectedComponent } from 'mailspring-component-kit';
|
|
|
|
import MessageParticipants from './message-participants';
|
|
import MessageItemBody from './message-item-body';
|
|
import MessageTimestamp from './message-timestamp';
|
|
import MessageControls from './message-controls';
|
|
|
|
interface MessageItemProps {
|
|
thread: Thread;
|
|
message: Message;
|
|
messages: Message[];
|
|
collapsed: boolean;
|
|
pending: boolean;
|
|
isMostRecent: boolean;
|
|
className: string;
|
|
}
|
|
|
|
interface MessageItemState {
|
|
filePreviewPaths: { [fileId: string]: string };
|
|
downloads: {
|
|
[fileId: string]: null;
|
|
};
|
|
detailedHeaders: boolean;
|
|
}
|
|
|
|
export default class MessageItem extends React.Component<MessageItemProps, MessageItemState> {
|
|
static displayName = 'MessageItem';
|
|
|
|
static propTypes = {
|
|
thread: PropTypes.object.isRequired,
|
|
message: PropTypes.object.isRequired,
|
|
messages: PropTypes.array.isRequired,
|
|
collapsed: PropTypes.bool,
|
|
pending: PropTypes.bool,
|
|
isMostRecent: PropTypes.bool,
|
|
className: PropTypes.string,
|
|
};
|
|
|
|
_headerEl: HTMLElement;
|
|
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
|
|
const fileIds = this.props.message.fileIds();
|
|
this.state = {
|
|
// Holds the downloadData (if any) for all of our files. It's a hash
|
|
// keyed by a fileId. The value is the downloadData.
|
|
downloads: AttachmentStore.getDownloadDataForFiles(fileIds),
|
|
filePreviewPaths: AttachmentStore.previewPathsForFiles(fileIds),
|
|
detailedHeaders: AppEnv.config.get('core.reading.detailedHeaders'),
|
|
};
|
|
}
|
|
|
|
private _storeUnlisten?: () => void;
|
|
|
|
componentDidMount() {
|
|
this._storeUnlisten = AttachmentStore.listen(this._onDownloadStoreChange);
|
|
}
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this._storeUnlisten) {
|
|
this._storeUnlisten();
|
|
}
|
|
}
|
|
|
|
_onClickParticipants = (e: React.MouseEvent<any>) => {
|
|
let el = e.target as HTMLElement;
|
|
while (el !== e.currentTarget) {
|
|
if (el.classList.contains('collapsed-participants')) {
|
|
this.setState({ detailedHeaders: true });
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
return;
|
|
};
|
|
|
|
_onClickHeader = e => {
|
|
this._onToggleCollapsed();
|
|
};
|
|
|
|
_onDownloadAll = () => {
|
|
Actions.fetchAndSaveAllFiles(this.props.message.files);
|
|
};
|
|
|
|
_onToggleCollapsed = () => {
|
|
if (this.props.isMostRecent) {
|
|
return;
|
|
}
|
|
Actions.toggleMessageIdExpanded(this.props.message.id);
|
|
};
|
|
|
|
_onDownloadStoreChange = () => {
|
|
const fileIds = this.props.message.fileIds();
|
|
this.setState({
|
|
downloads: AttachmentStore.getDownloadDataForFiles(fileIds),
|
|
filePreviewPaths: AttachmentStore.previewPathsForFiles(fileIds),
|
|
});
|
|
};
|
|
|
|
_renderDownloadAllButton() {
|
|
return (
|
|
<div className="download-all">
|
|
<div className="attachment-number">
|
|
<RetinaImg name="ic-attachments-all-clippy.png" mode={RetinaImg.Mode.ContentIsMask} />
|
|
<span>{`${this.props.message.files.length} ${localized('attachments')}`}</span>
|
|
</div>
|
|
<div className="separator">-</div>
|
|
<div className="download-all-action" onClick={this._onDownloadAll}>
|
|
<RetinaImg name="ic-attachments-download-all.png" mode={RetinaImg.Mode.ContentIsMask} />
|
|
<span>{localized('Download All')}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderAttachments() {
|
|
const { files = [], body, id } = this.props.message;
|
|
const { filePreviewPaths, downloads } = this.state;
|
|
let attachedFiles = files.filter(
|
|
f => !f.contentId || !(body || '').includes(`cid:${f.contentId}`)
|
|
);
|
|
|
|
for (const extension of MessageStore.extensions()) {
|
|
if (!extension.filterMessageFiles) continue;
|
|
attachedFiles = extension.filterMessageFiles({
|
|
message: this.props.message,
|
|
files: attachedFiles,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{files.length > 1 ? this._renderDownloadAllButton() : null}
|
|
{attachedFiles.length > 0 && (
|
|
<div className="attachments-area">
|
|
<InjectedComponent
|
|
matching={{ role: 'MessageAttachments' }}
|
|
exposedProps={{
|
|
files: attachedFiles,
|
|
messageId: id,
|
|
downloads,
|
|
filePreviewPaths,
|
|
canRemoveAttachments: false,
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderFooterStatus() {
|
|
return (
|
|
<InjectedComponentSet
|
|
className="message-footer-status"
|
|
matching={{ role: 'MessageFooterStatus' }}
|
|
exposedProps={{
|
|
message: this.props.message,
|
|
thread: this.props.thread,
|
|
detailedHeaders: this.state.detailedHeaders,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
_renderHeader() {
|
|
const { message, thread, messages, pending } = this.props;
|
|
|
|
return (
|
|
<header
|
|
ref={el => (this._headerEl = el)}
|
|
className={`message-header ${pending && 'pending'}`}
|
|
onClick={this._onClickHeader}
|
|
>
|
|
<InjectedComponent
|
|
matching={{ role: 'MessageHeader' }}
|
|
exposedProps={{ message: message, thread: thread, messages: messages }}
|
|
/>
|
|
<div className="pending-spinner" style={{ position: 'absolute', marginTop: -2 }}>
|
|
<RetinaImg width={18} name="sending-spinner.gif" mode={RetinaImg.Mode.ContentPreserve} />
|
|
</div>
|
|
<div className="message-header-right">
|
|
<MessageTimestamp
|
|
className="message-time"
|
|
isDetailed={this.state.detailedHeaders}
|
|
date={message.date}
|
|
/>
|
|
<InjectedComponentSet
|
|
className="message-header-status"
|
|
matching={{ role: 'MessageHeaderStatus' }}
|
|
exposedProps={{
|
|
message: message,
|
|
thread: thread,
|
|
detailedHeaders: this.state.detailedHeaders,
|
|
}}
|
|
/>
|
|
<MessageControls thread={thread} message={message} />
|
|
</div>
|
|
<MessageParticipants
|
|
from={message.from}
|
|
onClick={this._onClickParticipants}
|
|
isDetailed={this.state.detailedHeaders}
|
|
/>
|
|
<MessageParticipants
|
|
to={message.to}
|
|
cc={message.cc}
|
|
bcc={message.bcc}
|
|
replyTo={message.replyTo.filter(c => !message.from.find(fc => fc.email === c.email))}
|
|
onClick={this._onClickParticipants}
|
|
isDetailed={this.state.detailedHeaders}
|
|
/>
|
|
{this._renderFolder()}
|
|
{this._renderHeaderDetailToggle()}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
_renderHeaderDetailToggle() {
|
|
if (this.props.pending) {
|
|
return null;
|
|
}
|
|
if (this.state.detailedHeaders) {
|
|
return (
|
|
<div
|
|
className="header-toggle-control"
|
|
style={{ top: 18, left: -14 }}
|
|
onClick={e => {
|
|
this.setState({ detailedHeaders: false });
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
<RetinaImg
|
|
name={'message-disclosure-triangle-active.png'}
|
|
mode={RetinaImg.Mode.ContentIsMask}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="header-toggle-control inactive"
|
|
style={{ top: 18 }}
|
|
onClick={e => {
|
|
this.setState({ detailedHeaders: true });
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
<RetinaImg name={'message-disclosure-triangle.png'} mode={RetinaImg.Mode.ContentIsMask} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderFolder() {
|
|
if (!this.state.detailedHeaders) {
|
|
return false;
|
|
}
|
|
|
|
const folder = this.props.message.folder;
|
|
if (!folder || folder.role === 'al') {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<div className="header-row">
|
|
<div className="header-label">{localized('Folder')}: </div>
|
|
<div className="header-name">{folder.displayName}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderCollapsed() {
|
|
const {
|
|
message: { snippet, from, files, date, draft },
|
|
className,
|
|
} = this.props;
|
|
|
|
const attachmentIcon = Utils.showIconForAttachments(files) ? (
|
|
<div className="collapsed-attachment" />
|
|
) : null;
|
|
|
|
return (
|
|
<div className={className} onClick={this._onToggleCollapsed}>
|
|
<div className="message-item-white-wrap">
|
|
<div className="message-item-area">
|
|
<div className="collapsed-from">
|
|
{from &&
|
|
from[0] &&
|
|
from[0].displayName({ compact: !AppEnv.config.get('core.reading.detailedNames') })}
|
|
</div>
|
|
<div className="collapsed-snippet">{snippet}</div>
|
|
{draft && (
|
|
<div className="Collapsed-draft">
|
|
<RetinaImg
|
|
name="icon-draft-pencil.png"
|
|
className="draft-icon"
|
|
mode={RetinaImg.Mode.ContentPreserve}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="collapsed-timestamp">
|
|
<MessageTimestamp date={date} />
|
|
</div>
|
|
{attachmentIcon}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderFull() {
|
|
return (
|
|
<div className={this.props.className}>
|
|
<div className="message-item-white-wrap">
|
|
<div className="message-item-area">
|
|
{this._renderHeader()}
|
|
<MessageItemBody message={this.props.message} downloads={this.state.downloads} />
|
|
{this._renderAttachments()}
|
|
{this._renderFooterStatus()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return this.props.collapsed ? this._renderCollapsed() : this._renderFull();
|
|
}
|
|
}
|