mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-23 08:46:07 +08:00
b8ed562d19
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
246 lines
7.8 KiB
CoffeeScript
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: </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
|