mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-11 18:32:20 +08:00
d15b5080fb
Summary: ThreadStore is now in the thread-list package. Account sidebar no longer has random stuff dealing with search, no longer maintains selection apart from FocusedTagStore Thread nav buttons are in the thread package Account sidebar pulls selection from FocusedTagStore, no longer fires an Action to select Inbox, which was weird Thread store is in thread-list package. No longer has any selection concept -> moved to FocusedThreadStore. Also looks at database changes to do "shallow" updates when only threads and not messages have changed, or when only messages of a few... ...threads have changed. WorkspaceStore now handles both pushing AND popping the thread sheet. So all sheet behavior is here. ThreadStore => FocusedThreadStore, selectThreadId => selectThread Include all models in inbox-exports It actually takes a long time to call Promise.reject because Bluebird generates stack traces. Resolve with false instead (100msec faster!) Cache the model class map. All the requires take ~20msec per call to this method ThreadList looks at FocusedThreadStore for selection FocusedThreadStore, FocusedTagStore Updated specs Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1384
268 lines
9.2 KiB
CoffeeScript
Executable file
268 lines
9.2 KiB
CoffeeScript
Executable file
_ = require 'underscore-plus'
|
|
React = require 'react'
|
|
MessageItem = require "./message-item"
|
|
{Utils, Actions, MessageStore, ComponentRegistry} = require("inbox-exports")
|
|
{Spinner, ResizableRegion, RetinaImg} = require('ui-components')
|
|
|
|
module.exports =
|
|
MessageList = React.createClass
|
|
mixins: [ComponentRegistry.Mixin]
|
|
components: ['Participants', '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 MessageStore.itemsLoading()
|
|
@_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) ->
|
|
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">
|
|
<div className="message-list-notification-bars">
|
|
{@_messageListNotificationBars()}
|
|
</div>
|
|
|
|
{@_messageListHeaders()}
|
|
{@_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()
|
|
|
|
_messageListNotificationBars: ->
|
|
MLBars = ComponentRegistry.findAllViewsByRole('MessageListNotificationBar')
|
|
<div className="message-list-notification-bar-wrap">
|
|
{<MLBar thread={@state.currentThread} /> for MLBar in MLBars}
|
|
</div>
|
|
|
|
_messageListHeaders: ->
|
|
Participants = @state.Participants
|
|
MessageListHeaders = ComponentRegistry.findAllViewsByRole('MessageListHeader')
|
|
|
|
<div className="message-list-headers">
|
|
{for MessageListHeader in MessageListHeaders
|
|
<MessageListHeader thread={@state.currentThread} />
|
|
}
|
|
</div>
|
|
|
|
_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 className="message-item-divider collapsed" />
|
|
else
|
|
components.push <hr 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()
|
|
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
|