Mailspring/internal_packages/message-list/lib/message-item.cjsx
Ben Gotow 8f2211f6a0 feat(threads): List improvements and message collapsing
Summary:
This diff uses the new ?expanded=true threads request to fetch threads and the messages inside them at the same time. The messages from this endpoint don't contain bodies. Message bodies have been moved to a new "secondary attribute" type, which can be optionally requested when making queries. This allows us to 1) quickly fetch messages without worrying about MBs of JSON, 2) update messages without updating their bodies, and 3) avoid calls to /messages?thread_id=123. The new message store fetches just the items it wants to display in expanded mode, and we'll show snippets for the rest.

Fix up forwarded message

Approach: Thread.messageMetadata

join approach WIP

join approach complete

"" || null = null. OMG.

Make spinner a bit smarter, use code delays and not css delays

Search suggestion store should only show first 10 matches

Msg collapsing, refactored msg store that will fetch individual messages that are marked as expanded, set loaded = true when it's all done

Test Plan: Tests coming soon. The query refactoring here broke a lot of tests...

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1345
2015-03-25 12:41:48 -07:00

227 lines
8.3 KiB
CoffeeScript

React = require 'react'
_ = require 'underscore-plus'
EmailFrame = require './email-frame'
MessageParticipants = require "./message-participants"
MessageTimestamp = require "./message-timestamp"
{ComponentRegistry, FileDownloadStore, Utils, Actions} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
Autolinker = require 'autolinker'
TransparentPixel = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="
MessageBodyWidth = 740
module.exports =
MessageItem = React.createClass
displayName: 'MessageItem'
propTypes:
thread: React.PropTypes.object.isRequired
message: React.PropTypes.object.isRequired
thread_participants: React.PropTypes.arrayOf(React.PropTypes.object)
collapsed: React.PropTypes.bool
mixins: [ComponentRegistry.Mixin]
components: ['AttachmentComponent']
getInitialState: ->
# 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.downloadsForFileIds(@props.message.fileIds())
showQuotedText: @_isForwardedMessage()
detailedHeaders: false
componentDidMount: ->
@_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)
componentWillUnmount: ->
@_storeUnlisten() if @_storeUnlisten
shouldComponentUpdate: (nextProps, nextState) ->
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
render: ->
messageIndicators = ComponentRegistry.findAllViewsByRole('MessageIndicator')
attachments = @_attachmentComponents()
if attachments.length > 0
attachments = <div className="attachments-area">{attachments}</div>
header =
<header className="message-header">
<div className="message-header-right">
<MessageTimestamp className="message-time"
isDetailed={@state.detailedHeaders}
date={@props.message.date} />
{<div className="message-indicator"><Indicator message={@props.message}/></div> for Indicator in messageIndicators}
{if @state.detailedHeaders then @_renderMessageActionsInline() else @_renderMessageActionsTooltip()}
</div>
<MessageParticipants to={@props.message.to}
cc={@props.message.cc}
bcc={@props.message.bcc}
from={@props.message.from}
subject={@props.message.subject}
onClick={=> @setState detailedHeaders: true}
thread_participants={@props.thread_participants}
isDetailed={@state.detailedHeaders}
message_participants={@props.message.participants()} />
{@_renderCollapseControl()}
</header>
if @props.collapsed
<div className={@props.className} onClick={@_toggleCollapsed}>
<div className="message-item-area">
{header}
<div className="snippet">
{@props.message.snippet}
</div>
</div>
</div>
else
<div className={@props.className}>
<div className="message-item-area">
{header}
{attachments}
<EmailFrame showQuotedText={@state.showQuotedText}>
{@_formatBody()}
</EmailFrame>
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
</div>
</div>
_quotedTextClasses: -> React.addons.classSet
"quoted-text-control": true
'no-quoted-text': (Utils.quotedTextIndex(@props.message.body) is -1)
'show-quoted-text': @state.showQuotedText
_renderMessageActionsInline: ->
@_renderMessageActions()
_renderMessageActionsTooltip: ->
return <span></span>
## TODO: For now leave blank. There may be an alternative UI in the
#future.
# <span className="msg-actions-tooltip"
# onClick={=> @setState detailedHeaders: true}>
# <RetinaImg name={"message-show-more.png"}/></span>
_renderMessageActions: ->
messageActions = ComponentRegistry.findAllViewsByRole('MessageAction')
<div className="message-actions-wrap">
<div className="message-actions">
<button className="btn btn-icon" onClick={@_onReply}>
<RetinaImg name={"message-reply.png"}/>
</button>
<button className="btn btn-icon" onClick={@_onReplyAll}>
<RetinaImg name={"message-reply-all.png"}/>
</button>
<button className="btn btn-icon" onClick={@_onForward}>
<RetinaImg name={"message-forward.png"}/>
</button>
{<Action thread={@props.thread} message={@props.message} /> for Action in messageActions}
</div>
</div>
_onReply: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeReply(threadId: tId, messageId: mId) if (tId and mId)
_onReplyAll: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeReplyAll(threadId: tId, messageId: mId) if (tId and mId)
_onForward: ->
tId = @props.thread.id; mId = @props.message.id
Actions.composeForward(threadId: tId, messageId: mId) if (tId and mId)
_renderCollapseControl: ->
if @state.detailedHeaders
<div className="collapse-control"
style={top: "4px", left: "-17px"}
onClick={=> @setState detailedHeaders: false}>
<RetinaImg name={"message-disclosure-triangle-active.png"}/>
</div>
else
<div className="collapse-control inactive"
style={top: "3px"}
onClick={=> @setState detailedHeaders: true}>
<RetinaImg name={"message-disclosure-triangle.png"}/>
</div>
# Eventually, _formatBody will run a series of registered body transformers.
# For now, it just runs a few we've hardcoded here, which are all synchronous.
_formatBody: ->
return "" unless @props.message and @props.message.body
body = @props.message.body
# Apply the autolinker pass to make emails and links clickable
body = Autolinker.link(body, {twitter: false})
# Find inline images and give them a calculated CSS height based on
# html width and height, when available. This means nothing changes size
# as the image is loaded, and we can estimate final height correctly.
# Note that MessageBodyWidth must be updated if the UI is changed!
cidRegex = /src=['"]cid:([^'"]*)['"]/g
while (result = cidRegex.exec(body)) isnt null
imgstart = body.lastIndexOf('<', result.index)
imgend = body.indexOf('/>', result.index)
if imgstart != -1 and imgend > imgstart
imgtag = body.substr(imgstart, imgend - imgstart)
width = imgtag.match(/width[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
height = imgtag.match(/height[ ]?=[ ]?['"]?(\d*)['"]?/)?[1]
if width and height
scale = Math.min(1, MessageBodyWidth / width)
style = " style=\"height:#{height * scale}px;\" "
body = body.substr(0, imgend) + style + body.substr(imgend)
# Replace cid:// references with the paths to downloaded files
for file in @props.message.files
continue if _.find @state.downloads, (d) -> d.fileId is file.id
cidLink = "cid:#{file.contentId}"
fileLink = "#{FileDownloadStore.pathForFile(file)}"
body = body.replace(cidLink, fileLink)
# Replace remaining cid:// references - we will not display them since they'll
# throw "unknown ERR_UNKNOWN_URL_SCHEME". Show a transparent pixel so that there's
# no "missing image" region shown, just a space.
body = body.replace(/src=['"]cid:[^'"]*['"]/g, "src=\"#{TransparentPixel}\"")
body
_toggleQuotedText: ->
@setState
showQuotedText: !@state.showQuotedText
_toggleCollapsed: ->
Actions.toggleMessageIdExpanded(@props.message.id)
_formatContacts: (contacts=[]) ->
_attachmentComponents: ->
return [] unless @props.message.body
AttachmentComponent = @state.AttachmentComponent
attachments = _.filter @props.message.files, (f) =>
inBody = f.contentId? and @props.message.body.indexOf(f.contentId) > 0
not inBody and f.filename.length > 0
attachments.map (file) =>
<AttachmentComponent file={file} key={file.id} download={@state.downloads[file.id]}/>
_isForwardedMessage: ->
Utils.isForwardedMessage(@props.message)
_onDownloadStoreChange: ->
@setState
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())