mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 04:25:31 +08:00
342 lines
9.5 KiB
JavaScript
342 lines
9.5 KiB
JavaScript
import React, {PropTypes} from 'react'
|
|
import classNames from 'classnames'
|
|
import {
|
|
Utils,
|
|
Actions,
|
|
AttachmentStore,
|
|
} from 'nylas-exports'
|
|
import {
|
|
RetinaImg,
|
|
InjectedComponentSet,
|
|
InjectedComponent,
|
|
} from 'nylas-component-kit'
|
|
|
|
import MessageParticipants from "./message-participants"
|
|
import MessageItemBody from "./message-item-body"
|
|
import MessageTimestamp from "./message-timestamp"
|
|
import MessageControls from './message-controls'
|
|
|
|
|
|
export default class MessageItem extends React.Component {
|
|
static displayName = 'MessageItem';
|
|
|
|
static propTypes = {
|
|
thread: PropTypes.object.isRequired,
|
|
message: PropTypes.object.isRequired,
|
|
messages: PropTypes.array.isRequired,
|
|
collapsed: PropTypes.bool,
|
|
pending: PropTypes.bool,
|
|
isLastMsg: PropTypes.bool,
|
|
className: PropTypes.string,
|
|
};
|
|
|
|
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: false,
|
|
detailedHeadersTogglePos: {top: 18},
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._storeUnlisten = AttachmentStore.listen(this._onDownloadStoreChange);
|
|
this._setDetailedHeadersTogglePos();
|
|
}
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
this._setDetailedHeadersTogglePos();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this._storeUnlisten) {
|
|
this._storeUnlisten();
|
|
}
|
|
}
|
|
|
|
_onClickParticipants = (e) => {
|
|
let el = e.target;
|
|
while (el !== e.currentTarget) {
|
|
if (el.classList.contains("collapsed-participants")) {
|
|
this.setState({detailedHeaders: true});
|
|
e.stopPropagation();
|
|
return;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
return;
|
|
}
|
|
|
|
_onClickHeader = (e) => {
|
|
if (this.state.detailedHeaders) {
|
|
return;
|
|
}
|
|
let el = e.target;
|
|
while (el !== e.currentTarget) {
|
|
if (el.classList.contains("message-header-right") || el.classList.contains("collapsed-participants")) {
|
|
return;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
this._onToggleCollapsed();
|
|
}
|
|
|
|
_onDownloadAll = () => {
|
|
Actions.fetchAndSaveAllFiles(this.props.message.files);
|
|
}
|
|
|
|
_setDetailedHeadersTogglePos = () => {
|
|
if (!this._headerEl) { return; }
|
|
const fromNode = this._headerEl.querySelector('.participant-name.from-contact,.participant-primary')
|
|
if (!fromNode) { return; }
|
|
const fromRect = fromNode.getBoundingClientRect()
|
|
const topPos = Math.floor(fromNode.offsetTop + (fromRect.height / 2) - 10)
|
|
if (topPos !== this.state.detailedHeadersTogglePos.top) {
|
|
this.setState({detailedHeadersTogglePos: {top: topPos}});
|
|
}
|
|
}
|
|
|
|
_onToggleCollapsed = () => {
|
|
if (this.props.isLastMsg) {
|
|
return;
|
|
}
|
|
Actions.toggleMessageIdExpanded(this.props.message.id);
|
|
}
|
|
|
|
_isRealFile = (file) => {
|
|
const hasCIDInBody = file.contentId !== undefined && this.props.message.body && this.props.message.body.indexOf(file.contentId) > 0;
|
|
return !hasCIDInBody;
|
|
}
|
|
|
|
_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} 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>Download all</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderAttachments() {
|
|
const files = (this.props.message.files || []).filter((f) => this._isRealFile(f));
|
|
const messageId = this.props.message.id;
|
|
const {filePreviewPaths, downloads} = this.state;
|
|
if (files.length === 0) {
|
|
return (<div />);
|
|
}
|
|
return (
|
|
<div>
|
|
{files.length > 1 ? this._renderDownloadAllButton() : null}
|
|
<div className="attachments-area">
|
|
<InjectedComponent
|
|
matching={{role: 'MessageAttachments'}}
|
|
exposedProps={{files, downloads, filePreviewPaths, messageId, 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;
|
|
const classes = classNames({
|
|
"message-header": true,
|
|
"pending": pending,
|
|
});
|
|
|
|
return (
|
|
<header
|
|
ref={(el) => { this._headerEl = el }}
|
|
className={classes}
|
|
onClick={this._onClickHeader}
|
|
>
|
|
<InjectedComponent
|
|
matching={{role: "MessageHeader"}}
|
|
exposedProps={{message: message, thread: thread, messages: messages}}
|
|
/>
|
|
<div className="pending-spinner" style={{position: 'absolute', marginTop: -2}}>
|
|
<RetinaImg
|
|
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}
|
|
onClick={this._onClickParticipants}
|
|
isDetailed={this.state.detailedHeaders}
|
|
/>
|
|
{this._renderFolder()}
|
|
{this._renderHeaderDetailToggle()}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
|
|
_renderHeaderDetailToggle() {
|
|
if (this.props.pending) {
|
|
return null;
|
|
}
|
|
const {top} = this.state.detailedHeadersTogglePos
|
|
if (this.state.detailedHeaders) {
|
|
return (
|
|
<div
|
|
className="header-toggle-control"
|
|
style={{top, left: "-14px"}}
|
|
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}}
|
|
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">Folder: </div>
|
|
<div className="header-name">{folder.displayName}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderCollapsed() {
|
|
const {message: {snippet, from, files, date}, 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: true})}
|
|
</div>
|
|
<div className="collapsed-snippet">
|
|
{snippet}
|
|
</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();
|
|
}
|
|
}
|