mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-23 00:36:50 +08:00
8f2211f6a0
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
227 lines
8.3 KiB
CoffeeScript
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())
|