Mailspring/internal_packages/message-list/lib/message-list.cjsx
Ben Gotow 2d51c94f51 feat(registered-region): Replace everywhere injecting elements by Role with <RegisteredRegion>
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
2015-04-21 18:16:08 -07:00

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