@@ -277,6 +259,7 @@ class MessageItem extends React.Component
showQuotedText: !@state.showQuotedText
_toggleCollapsed: =>
+ return if @props.isLastMsg
Actions.toggleMessageIdExpanded(@props.message.id)
_formatContacts: (contacts=[]) =>
diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx
index e38499899..d625acadf 100755
--- a/internal_packages/message-list/lib/message-list.cjsx
+++ b/internal_packages/message-list/lib/message-list.cjsx
@@ -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()}
+
- else return
+ else return
_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
- else
- components.push
+ components.push @_renderReplyArea()
- components
+ return components
+
+ _renderMinifiedBundle: (bundle) ->
+
+ BUNDLE_HEIGHT = 36
+ lines = bundle.messages[0...10]
+ h = Math.round(BUNDLE_HEIGHT / lines.length)
+
+
@setState minified: false }
+ key={Utils.generateTempId()}>
+
{bundle.messages.length} older messages
+
+ {lines.map (msg, i) ->
+
}
+
+
+
+ _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() ? [])
diff --git a/internal_packages/message-list/lib/message-nav-title.cjsx b/internal_packages/message-list/lib/message-nav-title.cjsx
new file mode 100644
index 000000000..51b324598
--- /dev/null
+++ b/internal_packages/message-list/lib/message-nav-title.cjsx
@@ -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"
+
+
Actions.popSheet() }
+ className="message-nav-title">{title}
+
+ _onChange: => _.defer =>
+ @setState(@_getStateFromStores())
+
+ _getStateFromStores: =>
+ tagId: FocusedTagStore.tagId()
+
+module.exports = MessageNavTitle
diff --git a/internal_packages/message-list/lib/message-subject-item.cjsx b/internal_packages/message-list/lib/message-subject-item.cjsx
deleted file mode 100644
index 366035a50..000000000
--- a/internal_packages/message-list/lib/message-subject-item.cjsx
+++ /dev/null
@@ -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: =>
-
{@state.thread?.subject}
-
- _onChange: => _.defer =>
- @setState(@_getStateFromStores())
-
- _getStateFromStores: =>
- thread: FocusedContentStore.focused('thread')
-
-module.exports = MessageSubjectItem
diff --git a/internal_packages/message-list/spec/message-list-spec.cjsx b/internal_packages/message-list/spec/message-list-spec.cjsx
index 33f705937..38f96314e 100644
--- a/internal_packages/message-list/spec/message-list-spec.cjsx
+++ b/internal_packages/message-list/spec/message-list-spec.cjsx
@@ -175,15 +175,15 @@ describe "MessageList", ->
spyOn(MessageStore, "itemsLoading").andCallFake ->
false
- @message_list = TestUtils.renderIntoDocument(
)
- @message_list_node = React.findDOMNode(@message_list)
+ @messageList = TestUtils.renderIntoDocument(
)
+ @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'}
+ ]
diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less
index db36907d7..52ab46822 100644
--- a/internal_packages/message-list/stylesheets/message-list.less
+++ b/internal_packages/message-list/stylesheets/message-list.less
@@ -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;
diff --git a/src/components/button-dropdown.cjsx b/src/components/button-dropdown.cjsx
new file mode 100644
index 000000000..5f791ef0e
--- /dev/null
+++ b/src/components/button-dropdown.cjsx
@@ -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: =>
+
+
+ {@props.primaryItem}
+
+
+
+
+
+ {@props.secondaryItems.map (item) ->
+
{item}
+ }
+
+
+
+ _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
diff --git a/src/flux/stores/focused-tag-store.coffee b/src/flux/stores/focused-tag-store.coffee
index ee00743e5..d4197806c 100644
--- a/src/flux/stores/focused-tag-store.coffee
+++ b/src/flux/stores/focused-tag-store.coffee
@@ -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
diff --git a/src/flux/stores/message-store.coffee b/src/flux/stores/message-store.coffee
index dfd21ac74..2455b652b 100644
--- a/src/flux/stores/message-store.coffee
+++ b/src/flux/stores/message-store.coffee
@@ -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()
diff --git a/static/components/button-dropdown.less b/static/components/button-dropdown.less
new file mode 100644
index 000000000..8f5689b54
--- /dev/null
+++ b/static/components/button-dropdown.less
@@ -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;
+ }
+}
diff --git a/static/images/thread-list/icon-thread-disclosure@2x.png b/static/images/thread-list/icon-thread-disclosure@2x.png
new file mode 100644
index 000000000..a49a693f9
Binary files /dev/null and b/static/images/thread-list/icon-thread-disclosure@2x.png differ
diff --git a/static/images/thread-list/icon-thread-reply@2x.png b/static/images/thread-list/icon-thread-reply@2x.png
new file mode 100644
index 000000000..b04143fb8
Binary files /dev/null and b/static/images/thread-list/icon-thread-reply@2x.png differ
diff --git a/static/index.less b/static/index.less
index e11a7fac4..680b8e7b8 100644
--- a/static/index.less
+++ b/static/index.less
@@ -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";