Mailspring/packages/client-app/internal_packages/message-list/lib/message-item.jsx
2017-07-23 20:38:15 -04:00

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:&nbsp;</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();
}
}