mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 05:06:53 +08:00
feat(message): New Message List UI
Summary: Initial message list collapsing messages can be expanded explicitly styling message items composer UI and collapsing expanding and collapsing headers style new reply area adding in message controls Add message actions dropdown Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D1664
This commit is contained in:
parent
2a847d36bd
commit
7739f08e84
17 changed files with 643 additions and 154 deletions
|
@ -13,6 +13,7 @@ module.exports =
|
|||
RetinaImg: require '../src/components/retina-img'
|
||||
ListTabular: require '../src/components/list-tabular'
|
||||
DraggableImg: require '../src/components/draggable-img'
|
||||
ButtonDropdown: require '../src/components/button-dropdown'
|
||||
MultiselectList: require '../src/components/multiselect-list'
|
||||
MultiselectActionBar: require '../src/components/multiselect-action-bar'
|
||||
ResizableRegion: require '../src/components/resizable-region'
|
||||
|
|
|
@ -50,8 +50,8 @@
|
|||
width: 100%;
|
||||
max-width: @compose-width;
|
||||
margin: 0 auto;
|
||||
margin-top: @spacing-standard;
|
||||
padding: 0;
|
||||
padding-top: 5px;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
@ -200,6 +200,10 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.composer-content-wrap {
|
||||
margin-top: @spacing-standard;
|
||||
}
|
||||
|
||||
.composer-inner-wrap {
|
||||
|
||||
.composer-action-bar-wrap {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
MessageList = require "./message-list"
|
||||
MessageToolbarItems = require "./message-toolbar-items"
|
||||
MessageSubjectItem = require "./message-subject-item"
|
||||
MessageNavTitle = require "./message-nav-title"
|
||||
{ComponentRegistry,
|
||||
WorkspaceStore} = require 'nylas-exports'
|
||||
SidebarThreadParticipants = require "./sidebar-thread-participants"
|
||||
|
@ -16,15 +16,15 @@ module.exports =
|
|||
ComponentRegistry.register MessageToolbarItems,
|
||||
location: WorkspaceStore.Location.MessageList.Toolbar
|
||||
|
||||
ComponentRegistry.register MessageSubjectItem,
|
||||
ComponentRegistry.register MessageNavTitle,
|
||||
location: WorkspaceStore.Location.MessageList.Toolbar
|
||||
|
||||
ComponentRegistry.register SidebarThreadParticipants,
|
||||
location: WorkspaceStore.Location.MessageListSidebar
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister MessageToolbarItems
|
||||
ComponentRegistry.unregister MessageSubjectItem
|
||||
ComponentRegistry.unregister MessageList
|
||||
ComponentRegistry.unregister MessageNavTitle
|
||||
ComponentRegistry.unregister MessageToolbarItems
|
||||
|
||||
serialize: -> @state
|
||||
|
|
82
internal_packages/message-list/lib/message-controls.cjsx
Normal file
82
internal_packages/message-list/lib/message-controls.cjsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
React = require 'react'
|
||||
{Actions} = require 'nylas-exports'
|
||||
{RetinaImg, ButtonDropdown} = require 'nylas-component-kit'
|
||||
|
||||
class MessageControls extends React.Component
|
||||
@displayName: "MessageControls"
|
||||
@propTypes:
|
||||
thread: React.PropTypes.object.isRequired
|
||||
message: React.PropTypes.object.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
render: =>
|
||||
<div className="message-actions-wrap">
|
||||
<div className="message-actions-ellipsis" onClick={@_onShowActionsMenu}>
|
||||
<RetinaImg name={"message-actions-ellipsis.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
|
||||
<ButtonDropdown
|
||||
primaryItem={@_primaryMessageAction()}
|
||||
secondaryItems={@_secondaryMessageActions()}/>
|
||||
</div>
|
||||
|
||||
_primaryMessageAction: =>
|
||||
if @_replyType() is "reply"
|
||||
<span onClick={@_onReply}>
|
||||
<RetinaImg name="reply-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</span>
|
||||
else # if "reply-all"
|
||||
<span onClick={@_onReplyAll}>
|
||||
<RetinaImg name="reply-all-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</span>
|
||||
|
||||
_secondaryMessageActions: ->
|
||||
if @_replyType() is "reply"
|
||||
return [@_replyAllAction(), @_forwardAction()]
|
||||
else #if "reply-all"
|
||||
return [@_replyAction(), @_forwardAction()]
|
||||
|
||||
_forwardAction: ->
|
||||
<span onClick={@_onForward}>
|
||||
<RetinaImg name="forward-message-header.png" mode={RetinaImg.Mode.ContentIsMask}/> Forward
|
||||
</span>
|
||||
_replyAction: ->
|
||||
<span onClick={@_onReply}>
|
||||
<RetinaImg name="reply-footer.png" mode={RetinaImg.Mode.ContentIsMask}/> Reply
|
||||
</span>
|
||||
_replyAllAction: ->
|
||||
<span onClick={@_onReplyAll}>
|
||||
<RetinaImg name="reply-all-footer.png" mode={RetinaImg.Mode.ContentIsMask}/> Reply All
|
||||
</span>
|
||||
|
||||
_onReply: =>
|
||||
Actions.composeReply(thread: @props.thread, message: @props.message)
|
||||
|
||||
_onReplyAll: =>
|
||||
Actions.composeReplyAll(thread: @props.thread, message: @props.message)
|
||||
|
||||
_onForward: =>
|
||||
Actions.composeForward(thread: @props.thread, message: @props.message)
|
||||
|
||||
_replyType: =>
|
||||
if @props.message.cc.length is 0 and @props.message.to.length is 1
|
||||
return "reply"
|
||||
else return "reply-all"
|
||||
|
||||
module.exports = MessageControls
|
||||
|
||||
# <InjectedComponentSet className="message-actions"
|
||||
# inline={true}
|
||||
# matching={role:"MessageAction"}
|
||||
# exposedProps={thread:@props.thread, message: @props.message}>
|
||||
# <button className="btn btn-icon" onClick={@_onReply}>
|
||||
# <RetinaImg name={"message-reply.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
# </button>
|
||||
# <button className="btn btn-icon" onClick={@_onReplyAll}>
|
||||
# <RetinaImg name={"message-reply-all.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
# </button>
|
||||
# <button className="btn btn-icon" onClick={@_onForward}>
|
||||
# <RetinaImg name={"message-forward.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
# </button>
|
||||
# </InjectedComponentSet>
|
|
@ -4,6 +4,7 @@ _ = require 'underscore'
|
|||
EmailFrame = require './email-frame'
|
||||
MessageParticipants = require "./message-participants"
|
||||
MessageTimestamp = require "./message-timestamp"
|
||||
MessageControls = require './message-controls'
|
||||
{Utils,
|
||||
Actions,
|
||||
NylasAPI,
|
||||
|
@ -80,18 +81,18 @@ class MessageItem extends React.Component
|
|||
</div>
|
||||
|
||||
_renderHeader: =>
|
||||
<header className="message-header">
|
||||
<header className="message-header" onClick={@_onClickHeader}>
|
||||
|
||||
<div className="message-header-right">
|
||||
<MessageTimestamp className="message-time selectable"
|
||||
isDetailed={@state.detailedHeaders}
|
||||
date={@props.message.date} />
|
||||
|
||||
<MessageControls thread={@props.thread} message={@props.message}/>
|
||||
|
||||
<InjectedComponentSet className="message-indicator"
|
||||
matching={role: "MessageIndicator"}
|
||||
exposedProps={message: @props.message}/>
|
||||
|
||||
{if @state.detailedHeaders then @_renderMessageActionsInline() else @_renderMessageActionsTooltip()}
|
||||
</div>
|
||||
|
||||
<MessageParticipants to={@props.message.to}
|
||||
|
@ -99,15 +100,37 @@ class MessageItem extends React.Component
|
|||
bcc={@props.message.bcc}
|
||||
from={@props.message.from}
|
||||
subject={@props.message.subject}
|
||||
onClick={=> @setState detailedHeaders: true}
|
||||
onClick={@_onClickParticipants}
|
||||
thread_participants={@props.thread_participants}
|
||||
isDetailed={@state.detailedHeaders}
|
||||
message_participants={@props.message.participants()} />
|
||||
|
||||
{@_renderCollapseControl()}
|
||||
{@_renderHeaderExpansionControl()}
|
||||
|
||||
</header>
|
||||
|
||||
_onClickParticipants: (e) =>
|
||||
el = e.target
|
||||
while el isnt e.currentTarget
|
||||
if "collapsed-participants" in el.classList
|
||||
@setState detailedHeaders: true
|
||||
e.stopPropagation()
|
||||
return
|
||||
el = el.parentElement
|
||||
return
|
||||
|
||||
_onClickHeader: (e) =>
|
||||
return if @state.detailedHeaders
|
||||
el = e.target
|
||||
while el isnt e.currentTarget
|
||||
wl = ["message-header-right",
|
||||
"collapsed-participants",
|
||||
"header-toggle-control"]
|
||||
if "message-header-right" in el.classList then return
|
||||
if "collapsed-participants" in el.classList then return
|
||||
el = el.parentElement
|
||||
@_toggleCollapsed()
|
||||
|
||||
_renderAttachments: =>
|
||||
attachments = @_attachmentComponents()
|
||||
if attachments.length > 0
|
||||
|
@ -120,47 +143,6 @@ class MessageItem extends React.Component
|
|||
'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: =>
|
||||
<div className="message-actions-wrap">
|
||||
<div className="message-actions-ellipsis" onClick={@_onShowActionsMenu}>
|
||||
<RetinaImg name={"message-actions-ellipsis.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
<InjectedComponentSet className="message-actions"
|
||||
inline={true}
|
||||
matching={role:"MessageAction"}
|
||||
exposedProps={thread:@props.thread, message: @props.message}>
|
||||
<button className="btn btn-icon" onClick={@_onReply}>
|
||||
<RetinaImg name={"message-reply.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onReplyAll}>
|
||||
<RetinaImg name={"message-reply-all.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
<button className="btn btn-icon" onClick={@_onForward}>
|
||||
<RetinaImg name={"message-forward.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
</InjectedComponentSet>
|
||||
</div>
|
||||
|
||||
_onReply: =>
|
||||
Actions.composeReply(thread: @props.thread, message: @props.message)
|
||||
|
||||
_onReplyAll: =>
|
||||
Actions.composeReplyAll(thread: @props.thread, message: @props.message)
|
||||
|
||||
_onForward: =>
|
||||
Actions.composeForward(thread: @props.thread, message: @props.message)
|
||||
|
||||
_onReport: (issueType) =>
|
||||
{Contact, Message, DatabaseStore, NamespaceStore} = require 'nylas-exports'
|
||||
|
||||
|
@ -216,17 +198,17 @@ class MessageItem extends React.Component
|
|||
menu.append(new MenuItem({ label: 'Show Original', click: => @_onShowOriginal()}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
|
||||
_renderCollapseControl: =>
|
||||
_renderHeaderExpansionControl: =>
|
||||
if @state.detailedHeaders
|
||||
<div className="collapse-control"
|
||||
style={top: "4px", left: "-17px"}
|
||||
onClick={=> @setState detailedHeaders: false}>
|
||||
<div className="header-toggle-control"
|
||||
style={top: "18px", left: "-14px"}
|
||||
onClick={ (e) => @setState detailedHeaders: false; e.stopPropagation()}>
|
||||
<RetinaImg name={"message-disclosure-triangle-active.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
else
|
||||
<div className="collapse-control inactive"
|
||||
style={top: "3px"}
|
||||
onClick={=> @setState detailedHeaders: true}>
|
||||
<div className="header-toggle-control inactive"
|
||||
style={top: "18px"}
|
||||
onClick={ (e) => @setState detailedHeaders: true; e.stopPropagation()}>
|
||||
<RetinaImg name={"message-disclosure-triangle.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
|
||||
|
@ -277,6 +259,7 @@ class MessageItem extends React.Component
|
|||
showQuotedText: !@state.showQuotedText
|
||||
|
||||
_toggleCollapsed: =>
|
||||
return if @props.isLastMsg
|
||||
Actions.toggleMessageIdExpanded(@props.message.id)
|
||||
|
||||
_formatContacts: (contacts=[]) =>
|
||||
|
|
|
@ -52,6 +52,8 @@ class MessageList extends React.Component
|
|||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
@state.minified = true
|
||||
@MINIFY_THRESHOLD = 3
|
||||
|
||||
componentDidMount: =>
|
||||
window.addEventListener("resize", @_onResize)
|
||||
|
@ -85,7 +87,7 @@ class MessageList extends React.Component
|
|||
else if newDraftIds.length > 0
|
||||
@_focusDraft(@refs["composerItem-#{newDraftIds[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)
|
||||
|
@ -113,6 +115,7 @@ class MessageList extends React.Component
|
|||
scrollTooltipComponent={MessageListScrollTooltip}
|
||||
onScroll={_.debounce(@_cacheScrollPos, 100)}
|
||||
ref="messageWrap">
|
||||
{@_renderSubject()}
|
||||
<div className="headers" style={position:'relative'}>
|
||||
<InjectedComponentSet
|
||||
className="message-list-notification-bars"
|
||||
|
@ -125,19 +128,24 @@ class MessageList extends React.Component
|
|||
</div>
|
||||
{@_messageComponents()}
|
||||
</ScrollRegion>
|
||||
{@_renderReplyArea()}
|
||||
<Spinner visible={!@state.ready} />
|
||||
</div>
|
||||
|
||||
_renderSubject: ->
|
||||
<div className="message-subject-wrap">
|
||||
<div className="message-count">{@state.messages.length} {if @state.messages.length is 1 then "message" else "messages"}</div>
|
||||
<div className="message-subject">{@state.currentThread?.subject}</div>
|
||||
</div>
|
||||
|
||||
_renderReplyArea: =>
|
||||
if @_hasReplyArea()
|
||||
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea}>
|
||||
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key={Utils.generateTempId()}>
|
||||
<div className="footer-reply-area">
|
||||
<RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
<span className="reply-text">Write a reply…</span>
|
||||
</div>
|
||||
</div>
|
||||
else return <div></div>
|
||||
else return <div key={Utils.generateTempId()}></div>
|
||||
|
||||
_hasReplyArea: =>
|
||||
not _.last(@state.messages)?.draft
|
||||
|
@ -191,7 +199,13 @@ class MessageList extends React.Component
|
|||
threadParticipants = @_threadParticipants()
|
||||
components = []
|
||||
|
||||
@state.messages?.forEach (message, idx) =>
|
||||
messages = @_messagesWithMinification(@state.messages)
|
||||
messages.forEach (message, idx) =>
|
||||
|
||||
if message.type is "minifiedBundle"
|
||||
components.push(@_renderMinifiedBundle(message))
|
||||
return
|
||||
|
||||
collapsed = !@state.messagesExpandedState[message.id]
|
||||
|
||||
initialScroll = not appliedInitialScroll and not collapsed and
|
||||
|
@ -202,6 +216,7 @@ class MessageList extends React.Component
|
|||
|
||||
className = classNames
|
||||
"message-item-wrap": true
|
||||
"before-reply-area": (messages.length - 1 is idx) and @_hasReplyArea()
|
||||
"initial-scroll": initialScroll
|
||||
"unread": message.unread
|
||||
"draft": message.draft
|
||||
|
@ -219,17 +234,70 @@ class MessageList extends React.Component
|
|||
message={message}
|
||||
className={className}
|
||||
collapsed={collapsed}
|
||||
isLastMsg={(messages.length - 1 is idx)}
|
||||
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.push @_renderReplyArea()
|
||||
|
||||
components
|
||||
return components
|
||||
|
||||
_renderMinifiedBundle: (bundle) ->
|
||||
|
||||
BUNDLE_HEIGHT = 36
|
||||
lines = bundle.messages[0...10]
|
||||
h = Math.round(BUNDLE_HEIGHT / lines.length)
|
||||
|
||||
<div className="minified-bundle"
|
||||
onClick={ => @setState minified: false }
|
||||
key={Utils.generateTempId()}>
|
||||
<div className="num-messages">{bundle.messages.length} older messages</div>
|
||||
<div className="msg-lines" style={height: h*lines.length}>
|
||||
{lines.map (msg, i) ->
|
||||
<div style={height: h*2, top: -h*i} className="msg-line"></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_messagesWithMinification: (messages=[]) =>
|
||||
return messages unless @state.minified
|
||||
|
||||
messages = _.clone(messages)
|
||||
minifyRanges = []
|
||||
consecutiveCollapsed = 0
|
||||
|
||||
messages.forEach (message, idx) =>
|
||||
return if idx is 0 # Never minify the 1st message
|
||||
|
||||
expandState = @state.messagesExpandedState[message.id]
|
||||
|
||||
if not expandState
|
||||
consecutiveCollapsed += 1
|
||||
else
|
||||
# We add a +1 because we don't minify the last collapsed message,
|
||||
# but the MINIFY_THRESHOLD refers to the smallest N that can be in
|
||||
# the "N older messages" minified block.
|
||||
if expandState is "default"
|
||||
minifyOffset = 1
|
||||
else # if expandState is "explicit"
|
||||
minifyOffset = 0
|
||||
|
||||
if consecutiveCollapsed >= @MINIFY_THRESHOLD + minifyOffset
|
||||
minifyRanges.push
|
||||
start: idx - consecutiveCollapsed
|
||||
length: (consecutiveCollapsed - minifyOffset)
|
||||
consecutiveCollapsed = 0
|
||||
|
||||
indexOffset = 0
|
||||
for range in minifyRanges
|
||||
start = range.start - indexOffset
|
||||
minified =
|
||||
type: "minifiedBundle"
|
||||
messages: messages[start...(start+range.length)]
|
||||
messages.splice(start, range.length, minified)
|
||||
|
||||
# While we removed `range.length` items, we also added 1 back in.
|
||||
indexOffset += (range.length - 1)
|
||||
|
||||
return messages
|
||||
|
||||
# Some child components (like the composer) might request that we scroll
|
||||
# to a given location. If `selectionTop` is defined that means we should
|
||||
|
@ -256,7 +324,10 @@ class MessageList extends React.Component
|
|||
messageWrap = React.findDOMNode(@refs.messageWrap)
|
||||
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
newState = @_getStateFromStores()
|
||||
if @state.currentThread isnt newState.currentThread
|
||||
newState.minified = true
|
||||
@setState(newState)
|
||||
|
||||
_getStateFromStores: =>
|
||||
messages: (MessageStore.items() ? [])
|
||||
|
|
34
internal_packages/message-list/lib/message-nav-title.cjsx
Normal file
34
internal_packages/message-list/lib/message-nav-title.cjsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
|
||||
_ = require 'underscore'
|
||||
_str = require 'underscore.string'
|
||||
React = require 'react'
|
||||
{Actions, FocusedTagStore} = require 'nylas-exports'
|
||||
|
||||
class MessageNavTitle extends React.Component
|
||||
@displayName: 'MessageNavTitle'
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@_unsubscriber = FocusedTagStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_unsubscriber() if @_unsubscriber
|
||||
|
||||
render: =>
|
||||
if @state.tagId
|
||||
title = "Back to #{_str.titleize(@state.tagId)}"
|
||||
else
|
||||
title = "Back"
|
||||
|
||||
<div onClick={ -> Actions.popSheet() }
|
||||
className="message-nav-title">{title}</div>
|
||||
|
||||
_onChange: => _.defer =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
tagId: FocusedTagStore.tagId()
|
||||
|
||||
module.exports = MessageNavTitle
|
|
@ -1,26 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
{FocusedContentStore} = require 'nylas-exports'
|
||||
|
||||
class MessageSubjectItem extends React.Component
|
||||
@displayName: 'MessageSubjectItem'
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@_unsubscriber = FocusedContentStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_unsubscriber() if @_unsubscriber
|
||||
|
||||
render: =>
|
||||
<div className="message-toolbar-subject">{@state.thread?.subject}</div>
|
||||
|
||||
_onChange: => _.defer =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
thread: FocusedContentStore.focused('thread')
|
||||
|
||||
module.exports = MessageSubjectItem
|
|
@ -175,15 +175,15 @@ describe "MessageList", ->
|
|||
spyOn(MessageStore, "itemsLoading").andCallFake ->
|
||||
false
|
||||
|
||||
@message_list = TestUtils.renderIntoDocument(<MessageList />)
|
||||
@message_list_node = React.findDOMNode(@message_list)
|
||||
@messageList = TestUtils.renderIntoDocument(<MessageList />)
|
||||
@messageList_node = React.findDOMNode(@messageList)
|
||||
|
||||
it "renders into the document", ->
|
||||
expect(TestUtils.isCompositeComponentWithType(@message_list,
|
||||
expect(TestUtils.isCompositeComponentWithType(@messageList,
|
||||
MessageList)).toBe true
|
||||
|
||||
it "by default has zero children", ->
|
||||
items = TestUtils.scryRenderedComponentsWithType(@message_list,
|
||||
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
||||
MessageItem)
|
||||
|
||||
expect(items.length).toBe 0
|
||||
|
@ -193,71 +193,240 @@ describe "MessageList", ->
|
|||
MessageStore._items = testMessages
|
||||
MessageStore._expandItemsToDefault()
|
||||
MessageStore.trigger(MessageStore)
|
||||
@message_list.setState currentThread: test_thread
|
||||
@messageList.setState currentThread: test_thread
|
||||
|
||||
it "renders all the correct number of messages", ->
|
||||
items = TestUtils.scryRenderedComponentsWithType(@message_list,
|
||||
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
||||
MessageItem)
|
||||
expect(items.length).toBe 5
|
||||
|
||||
it "renders the correct number of expanded messages", ->
|
||||
msgs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "message-item-wrap collapsed")
|
||||
msgs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "message-item-wrap collapsed")
|
||||
expect(msgs.length).toBe 4
|
||||
|
||||
it "aggregates participants across all messages", ->
|
||||
expect(@message_list._threadParticipants().length).toBe 4
|
||||
expect(@message_list._threadParticipants()[0] instanceof Contact).toBe true
|
||||
expect(@messageList._threadParticipants().length).toBe 4
|
||||
expect(@messageList._threadParticipants()[0] instanceof Contact).toBe true
|
||||
|
||||
it "displays lists of participants on the page", ->
|
||||
items = TestUtils.scryRenderedComponentsWithType(@message_list,
|
||||
items = TestUtils.scryRenderedComponentsWithType(@messageList,
|
||||
MessageParticipants)
|
||||
expect(items.length).toBe 1
|
||||
|
||||
it "focuses new composers when a draft is added", ->
|
||||
spyOn(@message_list, "_focusDraft")
|
||||
msgs = @message_list.state.messages
|
||||
spyOn(@messageList, "_focusDraft")
|
||||
msgs = @messageList.state.messages
|
||||
|
||||
@message_list.setState
|
||||
@messageList.setState
|
||||
messages: msgs.concat(draftMessages)
|
||||
|
||||
expect(@message_list._focusDraft).toHaveBeenCalled()
|
||||
expect(@message_list._focusDraft.mostRecentCall.args[0].props.exposedProps.localId).toEqual(draftMessages[0].id)
|
||||
expect(@messageList._focusDraft).toHaveBeenCalled()
|
||||
expect(@messageList._focusDraft.mostRecentCall.args[0].props.exposedProps.localId).toEqual(draftMessages[0].id)
|
||||
|
||||
describe "MessageList with draft", ->
|
||||
beforeEach ->
|
||||
MessageStore._items = testMessages.concat draftMessages
|
||||
MessageStore.trigger(MessageStore)
|
||||
spyOn(@message_list, "_focusDraft")
|
||||
@message_list.setState(currentThread: test_thread)
|
||||
spyOn(@messageList, "_focusDraft")
|
||||
@messageList.setState(currentThread: test_thread)
|
||||
|
||||
it "renders the composer", ->
|
||||
items = TestUtils.scryRenderedComponentsWithTypeAndProps(@message_list, InjectedComponent, matching: {role:"Composer"})
|
||||
expect(@message_list.state.messages.length).toBe 6
|
||||
items = TestUtils.scryRenderedComponentsWithTypeAndProps(@messageList, InjectedComponent, matching: {role:"Composer"})
|
||||
expect(@messageList.state.messages.length).toBe 6
|
||||
expect(items.length).toBe 1
|
||||
|
||||
it "doesn't focus on initial load", ->
|
||||
expect(@message_list._focusDraft).not.toHaveBeenCalled()
|
||||
expect(@messageList._focusDraft).not.toHaveBeenCalled()
|
||||
|
||||
describe "reply type", ->
|
||||
it "prompts for a reply when there's only one participant", ->
|
||||
MessageStore._items = [m3, m5]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
expect(@message_list._replyType()).toBe "reply"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
@messageList.setState currentThread: test_thread
|
||||
expect(@messageList._replyType()).toBe "reply"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
||||
expect(cs.length).toBe 1
|
||||
|
||||
it "prompts for a reply-all when there's more then one participant", ->
|
||||
MessageStore._items = [m5, m3]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
expect(@message_list._replyType()).toBe "reply-all"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
@messageList.setState currentThread: test_thread
|
||||
expect(@messageList._replyType()).toBe "reply-all"
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
||||
expect(cs.length).toBe 1
|
||||
|
||||
it "hides the reply type if the last message is a draft", ->
|
||||
MessageStore._items = [m5, m3, draftMessages[0]]
|
||||
MessageStore.trigger()
|
||||
@message_list.setState currentThread: test_thread
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@message_list, "footer-reply-area")
|
||||
@messageList.setState currentThread: test_thread
|
||||
cs = TestUtils.scryRenderedDOMComponentsWithClass(@messageList, "footer-reply-area")
|
||||
expect(cs.length).toBe 0
|
||||
|
||||
describe "Message minification", ->
|
||||
beforeEach ->
|
||||
@messageList.MINIFY_THRESHOLD = 3
|
||||
@messageList.setState minified: true
|
||||
@messages = [
|
||||
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'}, {id: 'g'}
|
||||
]
|
||||
|
||||
it "ignores the first message if it's collapsed", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: false, f: false, g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
||||
},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "ignores the first message if it's expanded", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: "default", b: false, c: false, d: false, e: false, f: false, g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
||||
},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "doesn't minify the last collapsed message", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: false, f: "default", g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
||||
},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "allows explicitly expanded messages", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: false, f: "explicit", g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]
|
||||
},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "doesn't minify if the threshold isn't reached", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: "default", c: false, d: "default", e: false, f: "default", g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{id: 'b'},
|
||||
{id: 'c'},
|
||||
{id: 'd'},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "doesn't minify if the threshold isn't reached due to the rule about not minifying the last collapsed messages", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: "default", f: "default", g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{id: 'b'},
|
||||
{id: 'c'},
|
||||
{id: 'd'},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "minifies at the threshold if the message is explicitly expanded", ->
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: "explicit", f: "default", g: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(@messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
||||
},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{id: 'g'}
|
||||
]
|
||||
|
||||
it "can have multiple minification blocks", ->
|
||||
messages = [
|
||||
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},
|
||||
{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}
|
||||
]
|
||||
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: false, f: "default",
|
||||
g: false, h: false, i: false, j: false, k: false, l: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
||||
},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]
|
||||
},
|
||||
{id: 'k'},
|
||||
{id: 'l'}
|
||||
]
|
||||
|
||||
it "can have multiple minification blocks next to explicitly expanded messages", ->
|
||||
messages = [
|
||||
{id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},
|
||||
{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}
|
||||
]
|
||||
|
||||
@messageList.setState messagesExpandedState:
|
||||
a: false, b: false, c: false, d: false, e: "explicit", f: "default",
|
||||
g: false, h: false, i: false, j: false, k: "explicit", l: "default"
|
||||
|
||||
out = @messageList._messagesWithMinification(messages)
|
||||
expect(out).toEqual [
|
||||
{id: 'a'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]
|
||||
},
|
||||
{id: 'e'},
|
||||
{id: 'f'},
|
||||
{
|
||||
type: "minifiedBundle"
|
||||
messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]
|
||||
},
|
||||
{id: 'k'},
|
||||
{id: 'l'}
|
||||
]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@import "ui-mixins";
|
||||
|
||||
@message-max-width: 800px;
|
||||
@message-spacing: 6px;
|
||||
|
||||
.tag-picker {
|
||||
.menu {
|
||||
|
@ -26,11 +27,10 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.message-toolbar-subject {
|
||||
.message-nav-title {
|
||||
order:-99;
|
||||
cursor: default;
|
||||
color:@text-color-heading;
|
||||
-webkit-app-region: drag;
|
||||
margin:0;
|
||||
margin-top:11px;
|
||||
font-size: @font-size-h4;
|
||||
|
@ -57,10 +57,11 @@
|
|||
}
|
||||
|
||||
.mode-split {
|
||||
.message-toolbar-subject {
|
||||
margin-left:@padding-base-horizontal;
|
||||
.message-nav-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -72,6 +73,21 @@
|
|||
padding: 0;
|
||||
order: 2;
|
||||
|
||||
.message-subject-wrap {
|
||||
width: calc(~"100% - 12px");
|
||||
max-width: @message-max-width;
|
||||
margin: 11px auto 10px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.message-subject {
|
||||
font-size: @font-size-large;
|
||||
}
|
||||
.message-count {
|
||||
float: right;
|
||||
font-size: @font-size-small;
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
|
||||
.message-list-headers {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
|
@ -94,24 +110,46 @@
|
|||
overflow-y: auto;
|
||||
opacity:0;
|
||||
transition: opacity 0s;
|
||||
background: @background-secondary;
|
||||
}
|
||||
.messages-wrap.ready {
|
||||
opacity:1;
|
||||
transition: opacity .1s linear;
|
||||
}
|
||||
|
||||
.message-item-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.minified-bundle + .message-item-wrap {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
margin: 0 auto;
|
||||
padding: @spacing-standard 0 @spacing-double 0;
|
||||
.message-item-wrap {
|
||||
|
||||
transition: height 0.1s;
|
||||
position: relative;
|
||||
|
||||
max-width: @message-max-width;
|
||||
width: calc(~"100% - 12px");
|
||||
|
||||
margin: @message-spacing auto;
|
||||
padding: 0 0 5px 0;
|
||||
|
||||
background: @background-primary;
|
||||
border: 1px solid @border-secondary-bg;
|
||||
box-shadow: 0 1px 1.5px rgba(0,0,0,0.14);
|
||||
border-radius: 4px;
|
||||
|
||||
&:first-child { padding-top: 0; }
|
||||
|
||||
&.before-reply-area { margin-bottom: 0; }
|
||||
|
||||
&.collapsed {
|
||||
background-color: darken(@background-primary, 3%);
|
||||
padding-bottom: 0;
|
||||
background-color: darken(@background-primary, 2%);
|
||||
padding-top: 19px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 0;
|
||||
|
||||
&+.minified-bundle {
|
||||
margin-top: -@message-spacing
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed .message-item-area {
|
||||
|
@ -131,7 +169,7 @@
|
|||
|
||||
.collapsed-from {
|
||||
font-weight: @font-weight-semi-bold;
|
||||
color: @text-color;
|
||||
color: @text-color-very-subtle;
|
||||
// min-width: 60px;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
@ -142,7 +180,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
transition: all .125s ease-in-out;
|
||||
}
|
||||
|
||||
.collapsed-participants {
|
||||
padding: 10px 10px 10px 0;
|
||||
}
|
||||
|
||||
.message-item-divider {
|
||||
|
@ -159,15 +200,51 @@
|
|||
}
|
||||
}
|
||||
|
||||
.minified-bundle {
|
||||
margin-right: @message-spacing;
|
||||
margin-left: @message-spacing;
|
||||
position: relative;
|
||||
.num-messages {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -80px;
|
||||
margin-top: -12px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid @border-color-divider;
|
||||
width: 160px;
|
||||
background: @background-primary;
|
||||
text-align: center;
|
||||
color: @text-color-very-subtle;
|
||||
z-index: 2;
|
||||
background: @background-primary;
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.msg-lines {
|
||||
max-width: @message-max-width;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.msg-line {
|
||||
border-radius: 4px 4px 0 0;
|
||||
position: relative;
|
||||
border-top: 1px solid @border-color-divider;
|
||||
border-right: 1px solid @border-color-divider;
|
||||
border-left: 1px solid @border-color-divider;
|
||||
background-color: darken(@background-primary, 2%);
|
||||
box-shadow: 0px 1px 1px rgba(0,0,0,0.1)
|
||||
}
|
||||
}
|
||||
|
||||
.message-header {
|
||||
position: relative;
|
||||
font-size: @font-size-small;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
padding-bottom: @spacing-standard;
|
||||
padding-top: 5px;
|
||||
padding-top: 19px;
|
||||
|
||||
.message-actions-wrap {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@ -200,8 +277,10 @@
|
|||
}
|
||||
|
||||
.message-time {
|
||||
padding-top: 5px;
|
||||
z-index: 2; position: relative;
|
||||
display: inline-block;
|
||||
min-width: 125px;
|
||||
}
|
||||
.msg-actions-tooltip {
|
||||
display: inline-block;
|
||||
|
@ -214,9 +293,11 @@
|
|||
}
|
||||
|
||||
.message-header-right {
|
||||
z-index: 4; position: relative;
|
||||
z-index: 4;
|
||||
position: relative;
|
||||
float: right;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -225,7 +306,7 @@
|
|||
width: 100%;
|
||||
max-width: @message-max-width;
|
||||
margin: 0 auto;
|
||||
padding: 0 @spacing-double @spacing-standard @spacing-double;
|
||||
padding: 0 20px @spacing-standard 20px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
|
@ -236,22 +317,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
.collapse-region {
|
||||
width: calc(~"100% - 30px");
|
||||
height: 56px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.collapse-control {
|
||||
.header-toggle-control {
|
||||
&.inactive { display: none; }
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -1 * @spacing-standard;
|
||||
left: -1 * 13px;
|
||||
img { background: @text-color-very-subtle; }
|
||||
}
|
||||
.message-item-wrap:hover {
|
||||
.collapse-control.inactive { display: block; }
|
||||
.header-toggle-control.inactive { display: block; }
|
||||
}
|
||||
|
||||
.footer-reply-area-wrap {
|
||||
width: 100%;
|
||||
border-top: 1px solid @border-color-divider;
|
||||
width: calc(~"100% - 12px");
|
||||
max-width: @message-max-width;
|
||||
margin: -3px auto @spacing-double auto;
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
border: 1px solid @border-secondary-bg;
|
||||
box-shadow: 0 1px 1.5px rgba(0,0,0,0.14);
|
||||
border-top: 1px dashed @border-color-divider;
|
||||
border-radius: 0 0 4px 4px;
|
||||
background: @background-primary;
|
||||
|
||||
color: @text-color-very-subtle;
|
||||
|
|
38
src/components/button-dropdown.cjsx
Normal file
38
src/components/button-dropdown.cjsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
RetinaImg = require './retina-img'
|
||||
{Utils} = require 'nylas-exports'
|
||||
|
||||
React = require 'react'
|
||||
class ButtonDropdown extends React.Component
|
||||
@displayName: "MessageControls"
|
||||
@propTypes:
|
||||
primaryItem: React.PropTypes.element
|
||||
secondaryItems: React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = showing: false
|
||||
|
||||
render: =>
|
||||
<div ref="button" onBlur={@_onBlur} tabIndex={999} className="#{@props.className ? ''} button-dropdown" >
|
||||
<div className="primary-item">
|
||||
{@props.primaryItem}
|
||||
</div>
|
||||
<div className="secondary-picker" onClick={@_toggleDropdown}>
|
||||
<RetinaImg name={"icon-thread-disclosure.png"} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</div>
|
||||
<div className="secondary-items" style={display: if @state.showing then "block" else "none"}>
|
||||
{@props.secondaryItems.map (item) ->
|
||||
<div key={Utils.generateTempId()} className="secondary-item">{item}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_toggleDropdown: =>
|
||||
@setState showing: !@state.showing
|
||||
|
||||
_onBlur: (event) =>
|
||||
target = event.nativeEvent.relatedTarget
|
||||
if target? and React.findDOMNode(@refs.button).contains(target)
|
||||
return
|
||||
@setState showing: false
|
||||
|
||||
module.exports = ButtonDropdown
|
|
@ -27,8 +27,8 @@ FocusedTagStore = Reflux.createStore
|
|||
|
||||
@_setTag(tag)
|
||||
|
||||
_onSearchQueryCommitted: (query) ->
|
||||
if query
|
||||
_onSearchQueryCommitted: (_query) ->
|
||||
if _query
|
||||
@_oldTag = @_tag
|
||||
@_setTag(null)
|
||||
else if @_oldTag
|
||||
|
@ -40,7 +40,7 @@ FocusedTagStore = Reflux.createStore
|
|||
@trigger()
|
||||
|
||||
# Public Methods
|
||||
|
||||
|
||||
tag: ->
|
||||
@_tag
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ MessageStore = Reflux.createStore
|
|||
if @_itemsExpanded[id]
|
||||
delete @_itemsExpanded[id]
|
||||
else
|
||||
@_itemsExpanded[id] = true
|
||||
@_itemsExpanded[id] = "explicit"
|
||||
for item, idx in @_items
|
||||
if @_itemsExpanded[item.id] and not _.isString(item.body)
|
||||
@_fetchMessageIdFromAPI(item.id)
|
||||
|
@ -169,7 +169,7 @@ MessageStore = Reflux.createStore
|
|||
_expandItemsToDefault: ->
|
||||
for item, idx in @_items
|
||||
if item.unread or item.draft or idx is @_items.length - 1
|
||||
@_itemsExpanded[item.id] = true
|
||||
@_itemsExpanded[item.id] = "default"
|
||||
|
||||
_fetchMessages: ->
|
||||
namespace = NamespaceStore.current()
|
||||
|
|
36
static/components/button-dropdown.less
Normal file
36
static/components/button-dropdown.less
Normal file
|
@ -0,0 +1,36 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.button-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.primary-item {
|
||||
.btn();
|
||||
height: 32px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
padding-top: 7px;
|
||||
}
|
||||
.secondary-picker {
|
||||
.btn();
|
||||
height: 32px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-left: 0;
|
||||
padding: 7px 8px 3px 8px;
|
||||
}
|
||||
.secondary-items {
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 4px 0 4px 4px;
|
||||
padding: 4px 15px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.55);
|
||||
z-index: 2;
|
||||
background-color: @background-primary;;
|
||||
position: absolute;
|
||||
width: 110px;
|
||||
right: 0;
|
||||
}
|
||||
img {
|
||||
background-color: @text-color;
|
||||
}
|
||||
}
|
BIN
static/images/thread-list/icon-thread-disclosure@2x.png
Normal file
BIN
static/images/thread-list/icon-thread-disclosure@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 333 B |
BIN
static/images/thread-list/icon-thread-reply@2x.png
Normal file
BIN
static/images/thread-list/icon-thread-reply@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 699 B |
|
@ -18,6 +18,7 @@
|
|||
@import "components/tokenizing-text-field";
|
||||
@import "components/extra";
|
||||
@import "components/list-tabular";
|
||||
@import "components/button-dropdown";
|
||||
@import "components/scroll-region";
|
||||
@import "components/spinner";
|
||||
@import "components/generated-form";
|
||||
|
|
Loading…
Reference in a new issue