mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-22 08:16:09 +08:00
2d51c94f51
Summary: This is a small diff that isolates the pattern of "insert all the things with role=ThreadAction and pass them these props". The goal is to eventually make this wrapper do some analysis, performance checks, etc. Right now, it just has the advantage of making the parent components simpler and also adding a new inspector view where you can see the regions , their names and the props they take (thread:<Thread> or selection:<ModelListSelection>) fix(menu): Re-importing different class with same name breaking dropdowns make composer use flexbox for it's footer fix alignment of the input in the participant text field. how did this break? new action for hiding / showing component regions Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1422
262 lines
9 KiB
CoffeeScript
Executable file
262 lines
9 KiB
CoffeeScript
Executable file
_ = require 'underscore-plus'
|
|
React = require 'react'
|
|
MessageItem = require "./message-item"
|
|
{Utils, Actions, MessageStore, ComponentRegistry} = require("inbox-exports")
|
|
{Spinner, ResizableRegion, RetinaImg, RegisteredRegion} = require('ui-components')
|
|
|
|
module.exports =
|
|
MessageList = React.createClass
|
|
mixins: [ComponentRegistry.Mixin]
|
|
components: ['Composer']
|
|
displayName: 'MessageList'
|
|
|
|
getInitialState: ->
|
|
@_getStateFromStores()
|
|
|
|
componentDidMount: ->
|
|
@__onResize = _.bind @_onResize, @
|
|
window.addEventListener("resize", @__onResize)
|
|
@_unsubscribers = []
|
|
@_unsubscribers.push MessageStore.listen @_onChange
|
|
|
|
# We don't need to listen to ThreadStore bcause MessageStore already
|
|
# listens to thead selection changes
|
|
|
|
if not @state.loading
|
|
@_prepareContentForDisplay()
|
|
|
|
componentWillUnmount: ->
|
|
unsubscribe() for unsubscribe in @_unsubscribers
|
|
window.removeEventListener("resize", @__onResize)
|
|
|
|
shouldComponentUpdate: (nextProps, nextState) ->
|
|
not Utils.isEqualReact(nextProps, @props) or
|
|
not Utils.isEqualReact(nextState, @state)
|
|
|
|
componentDidUpdate: (prevProps, prevState) ->
|
|
return if @state.loading
|
|
|
|
if prevState.loading
|
|
@_prepareContentForDisplay()
|
|
else
|
|
newDraftIds = @_newDraftIds(prevState)
|
|
newMessageIds = @_newMessageIds(prevState)
|
|
if newDraftIds.length > 0
|
|
@_focusDraft(@refs["composerItem-#{newDraftIds[0]}"])
|
|
@_prepareContentForDisplay()
|
|
else if newMessageIds.length > 0
|
|
@_prepareContentForDisplay()
|
|
|
|
_newDraftIds: (prevState) ->
|
|
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
|
|
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
|
|
return _.difference(newDraftIds, oldDraftIds) ? []
|
|
|
|
_newMessageIds: (prevState) ->
|
|
oldMessageIds = _.map(_.reject((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
|
|
newMessageIds = _.map(_.reject((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
|
|
return _.difference(newMessageIds, oldMessageIds) ? []
|
|
|
|
_focusDraft: (draftDOMNode) ->
|
|
# We need a 100ms delay so the DOM can finish painting the elements on
|
|
# the page. The focus doesn't work for some reason while the paint is in
|
|
# process.
|
|
_.delay =>
|
|
return unless @isMounted
|
|
draftDOMNode.focus()
|
|
,100
|
|
|
|
render: ->
|
|
return <div></div> if not @state.currentThread?
|
|
|
|
wrapClass = React.addons.classSet
|
|
"messages-wrap": true
|
|
"ready": @state.ready
|
|
|
|
<div className="message-list" id="message-list">
|
|
<div tabIndex="-1"
|
|
className={wrapClass}
|
|
onScroll={_.debounce(@_cacheScrollPos, 100)}
|
|
ref="messageWrap">
|
|
|
|
<RegisteredRegion className="message-list-notification-bars"
|
|
location="MessageListNotificationBar"
|
|
|
|
thread={@state.currentThread}/>
|
|
<RegisteredRegion className="message-list-headers"
|
|
location="MessageListHeaders"
|
|
thread={@state.currentThread}/>
|
|
|
|
{@_messageComponents()}
|
|
</div>
|
|
{@_renderReplyArea()}
|
|
<Spinner visible={!@state.ready} />
|
|
</div>
|
|
|
|
_renderReplyArea: ->
|
|
if @_hasReplyArea()
|
|
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea}>
|
|
<div className="footer-reply-area">
|
|
<RetinaImg name="#{@_replyType()}-footer.png" /><span className="reply-text">Write a reply…</span>
|
|
</div>
|
|
</div>
|
|
else return <div></div>
|
|
|
|
_hasReplyArea: ->
|
|
not _.last(@state.messages)?.draft
|
|
|
|
# Either returns "reply" or "reply-all"
|
|
_replyType: ->
|
|
lastMsg = _.last(_.filter((@state.messages ? []), (m) -> not m.draft))
|
|
if lastMsg?.cc.length is 0 and lastMsg?.to.length is 1
|
|
return "reply"
|
|
else return "reply-all"
|
|
|
|
_onClickReplyArea: ->
|
|
return unless @state.currentThread?.id
|
|
if @_replyType() is "reply-all"
|
|
Actions.composeReplyAll(threadId: @state.currentThread.id)
|
|
else
|
|
Actions.composeReply(threadId: @state.currentThread.id)
|
|
|
|
# There may be a lot of iframes to load which may take an indeterminate
|
|
# amount of time. As long as there is more content being painted onto
|
|
# the page and our height is changing, keep waiting. Then scroll to message.
|
|
scrollToMessage: (msgDOMNode, done, location="top", stability=5) ->
|
|
return done() unless msgDOMNode?
|
|
|
|
messageWrap = @refs.messageWrap?.getDOMNode()
|
|
lastHeight = -1
|
|
stableCount = 0
|
|
scrollIfSettled = =>
|
|
return unless @isMounted()
|
|
|
|
messageWrapHeight = messageWrap.getBoundingClientRect().height
|
|
if messageWrapHeight isnt lastHeight
|
|
lastHeight = messageWrapHeight
|
|
stableCount = 0
|
|
else
|
|
stableCount += 1
|
|
if stableCount is stability
|
|
if location is "top"
|
|
messageWrap.scrollTop = msgDOMNode.offsetTop
|
|
else if location is "bottom"
|
|
offsetTop = msgDOMNode.offsetTop
|
|
messageHeight = msgDOMNode.getBoundingClientRect().height
|
|
messageWrap.scrollTop = offsetTop - (messageWrapHeight - messageHeight)
|
|
return done()
|
|
|
|
window.requestAnimationFrame -> scrollIfSettled(msgDOMNode, done)
|
|
|
|
scrollIfSettled()
|
|
|
|
_messageComponents: ->
|
|
ComposerItem = @state.Composer
|
|
appliedInitialFocus = false
|
|
components = []
|
|
|
|
@state.messages?.forEach (message, idx) =>
|
|
collapsed = !@state.messagesExpandedState[message.id]
|
|
|
|
initialFocus = not appliedInitialFocus and not collapsed and
|
|
((message.draft) or
|
|
(message.unread) or
|
|
(idx is @state.messages.length - 1 and idx > 0))
|
|
appliedInitialFocus ||= initialFocus
|
|
|
|
className = React.addons.classSet
|
|
"message-item-wrap": true
|
|
"initial-focus": initialFocus
|
|
"unread": message.unread
|
|
"draft": message.draft
|
|
"collapsed": collapsed
|
|
|
|
if message.draft
|
|
components.push <ComposerItem mode="inline"
|
|
ref="composerItem-#{message.id}"
|
|
key={@state.messageLocalIds[message.id]}
|
|
localId={@state.messageLocalIds[message.id]}
|
|
onRequestScrollTo={@_onRequestScrollToComposer}
|
|
className={className} />
|
|
else
|
|
components.push <MessageItem key={message.id}
|
|
thread={@state.currentThread}
|
|
message={message}
|
|
className={className}
|
|
collapsed={collapsed}
|
|
thread_participants={@_threadParticipants()} />
|
|
|
|
if idx < @state.messages.length - 1
|
|
next = @state.messages[idx + 1]
|
|
nextCollapsed = next and !@state.messagesExpandedState[next.id]
|
|
if collapsed and nextCollapsed
|
|
components.push <hr key={idx} className="message-item-divider collapsed" />
|
|
else
|
|
components.push <hr key={idx} className="message-item-divider" />
|
|
|
|
components
|
|
|
|
# Some child components (like the compser) might request that we scroll
|
|
# to the bottom of the component.
|
|
_onRequestScrollToComposer: ({messageId, location}={}) ->
|
|
return unless @isMounted()
|
|
done = ->
|
|
location ?= "bottom"
|
|
composer = @refs["composerItem-#{messageId}"]?.getDOMNode()
|
|
@scrollToMessage(composer, done, location, 1)
|
|
|
|
_onChange: ->
|
|
@setState(@_getStateFromStores())
|
|
|
|
_getStateFromStores: ->
|
|
messages: (MessageStore.items() ? [])
|
|
messageLocalIds: MessageStore.itemLocalIds()
|
|
messagesExpandedState: MessageStore.itemsExpandedState()
|
|
currentThread: MessageStore.thread()
|
|
loading: MessageStore.itemsLoading()
|
|
ready: if MessageStore.itemsLoading() then false else @state?.ready ? false
|
|
|
|
_prepareContentForDisplay: ->
|
|
_.delay =>
|
|
return unless @isMounted()
|
|
focusedMessage = @getDOMNode().querySelector(".initial-focus")
|
|
@scrollToMessage focusedMessage, =>
|
|
@setState(ready: true)
|
|
@_cacheScrollPos()
|
|
, 100
|
|
|
|
_threadParticipants: ->
|
|
# We calculate the list of participants instead of grabbing it from
|
|
# `@state.currentThread.participants` because it makes it easier to
|
|
# test, is a better source of ground truth, and saves us from more
|
|
# dependencies.
|
|
participants = {}
|
|
for msg in (@state.messages ? [])
|
|
contacts = msg.participants()
|
|
for contact in contacts
|
|
if contact? and contact.email?.length > 0
|
|
participants[contact.email] = contact
|
|
return _.values(participants)
|
|
|
|
_onResize: (event) ->
|
|
return unless @isMounted()
|
|
@_scrollToBottom() if @_wasAtBottom()
|
|
@_cacheScrollPos()
|
|
|
|
_scrollToBottom: ->
|
|
messageWrap = @refs.messageWrap?.getDOMNode()
|
|
messageWrap.scrollTop = messageWrap.scrollHeight
|
|
|
|
_cacheScrollPos: ->
|
|
messageWrap = @refs.messageWrap?.getDOMNode()
|
|
return unless messageWrap
|
|
@_lastScrollTop = messageWrap.scrollTop
|
|
@_lastHeight = messageWrap.getBoundingClientRect().height
|
|
@_lastScrollHeight = messageWrap.scrollHeight
|
|
|
|
_wasAtBottom: ->
|
|
(@_lastScrollTop + @_lastHeight) >= @_lastScrollHeight
|
|
|
|
MessageList.minWidth = 500
|
|
MessageList.maxWidth = 900
|