Mailspring/internal_packages/message-list/lib/message-item.cjsx
Juan Tejada b8ed562d19 🎨(attachments): DRY and cleanup attachment components code
Summary:
The attachment components were the only React Components which used
inheritance between components, which is an anti-pattern in react. I
deleted these components in favor of new purely functional/dumb
components exposed via the component-kit: Attachment Item and
ImageAttachmentItem. These are defined in the same file to reuse some
smaller components between them, like the progress-bar, etc.

The attachments pacakage still remains, and only registers a single component to
a new are called MessageAttachments. This InjectedComponent role is
shared by the Composer and MessageItem, and is the only reason this
exists as an injected component in a separate package.
MessageAttachments renders all image and non image attachments for a
message or draft, and binds the appropriate actions for removal, downloading, etc.

The composer still used FileUpload and ImageUpload components for rendering
uploads in the Composer (i.e. when you add an attachment (these are
different from files because they aren't saved until the draft is
sent)). These 2 components were pretty much copied and pasted from the
ones in the attachments package, with subtle differences-- I got rid of
these as well in favor of the new AttachmentItem and ImageAttachmentItem

Also convert more coffee to ES6!

Test Plan: Unit tests

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D3381
2016-10-31 17:26:20 -07:00

246 lines
7.8 KiB
CoffeeScript

React = require 'react'
classNames = require 'classnames'
_ = require 'underscore'
EmailFrame = require('./email-frame').default
MessageParticipants = require "./message-participants"
MessageItemBody = require "./message-item-body"
MessageTimestamp = require("./message-timestamp").default
MessageControls = require './message-controls'
{Utils,
Actions,
MessageUtils,
AccountStore,
MessageStore,
MessageBodyProcessor,
QuotedHTMLTransformer,
ComponentRegistry,
FileDownloadStore} = require 'nylas-exports'
{RetinaImg,
InjectedComponentSet,
InjectedComponent} = require 'nylas-component-kit'
TransparentPixel = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="
class MessageItem extends React.Component
@displayName = 'MessageItem'
@propTypes =
thread: React.PropTypes.object.isRequired
message: React.PropTypes.object.isRequired
collapsed: React.PropTypes.bool
constructor: (@props) ->
@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: FileDownloadStore.downloadDataForFiles(@props.message.fileIds())
detailedHeaders: false
componentDidMount: =>
@_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)
componentWillUnmount: =>
@_storeUnlisten() if @_storeUnlisten
shouldComponentUpdate: (nextProps, nextState) =>
not Utils.isEqualReact(nextProps, @props) or
not Utils.isEqualReact(nextState, @state)
render: =>
if @props.collapsed
@_renderCollapsed()
else
@_renderFull()
_renderCollapsed: =>
attachmentIcon = []
if Utils.showIconForAttachments(@props.message.files)
attachmentIcon = <div className="collapsed-attachment"></div>
<div className={@props.className} onClick={@_toggleCollapsed}>
<div className="message-item-white-wrap">
<div className="message-item-area">
<div className="collapsed-from">
{@props.message.from?[0]?.displayName(compact: true)}
</div>
<div className="collapsed-snippet">
{@props.message.snippet}
</div>
<div className="collapsed-timestamp">
<MessageTimestamp date={@props.message.date} />
</div>
{attachmentIcon}
</div>
</div>
</div>
_renderFull: =>
<div className={@props.className}>
<div className="message-item-white-wrap">
<div className="message-item-area">
{@_renderHeader()}
<MessageItemBody message={@props.message} downloads={@state.downloads} />
{@_renderAttachments()}
{@_renderFooterStatus()}
</div>
</div>
</div>
_renderHeader: =>
classes = classNames
"message-header": true
"pending": @props.pending
<header className={classes} onClick={@_onClickHeader}>
{@_renderHeaderSideItems()}
<div className="message-header-right">
<MessageTimestamp className="message-time"
isDetailed={@state.detailedHeaders}
date={@props.message.date} />
<InjectedComponentSet
className="message-header-status"
matching={role:"MessageHeaderStatus"}
exposedProps={message: @props.message, thread: @props.thread, detailedHeaders: @state.detailedHeaders} />
<MessageControls thread={@props.thread} message={@props.message}/>
</div>
{@_renderFromParticipants()}
{@_renderToParticipants()}
{@_renderFolder()}
{@_renderHeaderDetailToggle()}
</header>
_renderFromParticipants: =>
<MessageParticipants
from={@props.message.from}
onClick={@_onClickParticipants}
isDetailed={@state.detailedHeaders} />
_renderToParticipants: =>
<MessageParticipants
to={@props.message.to}
cc={@props.message.cc}
bcc={@props.message.bcc}
onClick={@_onClickParticipants}
isDetailed={@state.detailedHeaders} />
_renderFolder: =>
return [] unless @state.detailedHeaders
acct = AccountStore.accountForId(@props.message.accountId)
acctUsesFolders = acct and acct.usesFolders()
folder = @props.message.categories?[0]
return unless folder and acctUsesFolders
<div className="header-row">
<div className="header-label">Folder:&nbsp;</div>
<div className="header-name">{folder.displayName}</div>
</div>
_onClickParticipants: (e) =>
el = e.target
while el isnt e.currentTarget
if "collapsed-participants" in el.classList
@setState(detailedHeaders: true)
e.stopPropagation()
return
el = el.parentElement
return
_onClickHeader: (e) =>
return if @state.detailedHeaders
el = e.target
while el isnt e.currentTarget
wl = ["message-header-right",
"collapsed-participants",
"header-toggle-control"]
if "message-header-right" in el.classList then return
if "collapsed-participants" in el.classList then return
el = el.parentElement
@_toggleCollapsed()
_onDownloadAll: =>
Actions.fetchAndSaveAllFiles(@props.message.files)
_renderDownloadAllButton: =>
<div className="download-all">
<div className="attachment-number">
<RetinaImg
name="ic-attachments-all-clippy.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
<span>{@props.message.files.length} attachments</span>
</div>
<div className="separator">-</div>
<div className="download-all-action" onClick={@_onDownloadAll}>
<RetinaImg
name="ic-attachments-download-all.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
<span>Download all</span>
</div>
</div>
_renderAttachments: =>
files = (@props.message.files ? []).filter((f) => @_isRealFile(f))
messageClientId = @props.message.clientId
downloadsData = @state.downloads
if files.length > 0
<div>
{if files.length > 1 then @_renderDownloadAllButton()}
<div className="attachments-area">
<InjectedComponent
matching={{role: 'MessageAttachments'}}
exposedProps={{files, messageClientId, downloadsData, canRemoveAttachments: false}}
/>
</div>
</div>
else
<div />
_renderFooterStatus: =>
<InjectedComponentSet
className="message-footer-status"
matching={role:"MessageFooterStatus"}
exposedProps={message: @props.message, thread: @props.thread, detailedHeaders: @state.detailedHeaders} />
_renderHeaderSideItems: ->
styles =
position: "absolute"
marginTop: -2
<div className="pending-spinner" style={styles}>
<RetinaImg ref="spinner"
name="sending-spinner.gif"
mode={RetinaImg.Mode.ContentPreserve}/>
</div>
_renderHeaderDetailToggle: =>
return null if @props.pending
if @state.detailedHeaders
<div className="header-toggle-control"
style={top: "18px", left: "-14px"}
onClick={ (e) => @setState(detailedHeaders: false); e.stopPropagation()}>
<RetinaImg name={"message-disclosure-triangle-active.png"} mode={RetinaImg.Mode.ContentIsMask}/>
</div>
else
<div className="header-toggle-control inactive"
style={top: "18px"}
onClick={ (e) => @setState(detailedHeaders: true); e.stopPropagation()}>
<RetinaImg name={"message-disclosure-triangle.png"} mode={RetinaImg.Mode.ContentIsMask}/>
</div>
_toggleCollapsed: =>
return if @props.isLastMsg
Actions.toggleMessageIdExpanded(@props.message.id)
_isRealFile: (file) ->
hasCIDInBody = file.contentId? and @props.message.body?.indexOf(file.contentId) > 0
return not hasCIDInBody
_onDownloadStoreChange: =>
@setState
downloads: FileDownloadStore.downloadDataForFiles(@props.message.fileIds())
module.exports = MessageItem