From e1ec298d4b32f9169e647905a15d31eb9348d257 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Tue, 24 Mar 2015 16:57:24 -0400 Subject: [PATCH] feat(messages): floating reply area in message-list Summary: reply text now scrolls to bottom on new draft tests for reply type Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1338 --- .../message-list/lib/message-item.cjsx | 38 ++-- .../message-list/lib/message-list.cjsx | 34 ++- .../lib/message-participants.cjsx | 8 +- .../message-list/spec/message-list-spec.cjsx | 197 ++++++++++-------- .../stylesheets/message-list.less | 102 +++++++-- .../forward-message-header@2x.png | Bin 0 -> 348 bytes .../message-list/reply-all-footer@2x.png | Bin 0 -> 894 bytes .../reply-all-message-header@2x.png | Bin 0 -> 921 bytes .../images/message-list/reply-footer@2x.png | Bin 0 -> 537 bytes .../message-list/reply-message-header@2x.png | Bin 0 -> 547 bytes static/workspace.less | 6 +- 11 files changed, 251 insertions(+), 134 deletions(-) create mode 100644 static/images/message-list/forward-message-header@2x.png create mode 100644 static/images/message-list/reply-all-footer@2x.png create mode 100644 static/images/message-list/reply-all-message-header@2x.png create mode 100644 static/images/message-list/reply-footer@2x.png create mode 100644 static/images/message-list/reply-message-header@2x.png diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx index 446ccc4d3..03cb5d792 100644 --- a/internal_packages/message-list/lib/message-item.cjsx +++ b/internal_packages/message-list/lib/message-item.cjsx @@ -92,26 +92,30 @@ MessageItem = React.createClass @_renderMessageActions() _renderMessageActionsTooltip: -> - ## TODO: Use Tooltip UI Component - @setState detailedHeaders: true}> - + return + ## TODO: For now leave blank. There may be an alternative UI in the + #future. + # @setState detailedHeaders: true}> + # _renderMessageActions: -> messageActions = ComponentRegistry.findAllViewsByRole('MessageAction') -
- - - +
+
+ + + - { for Action in messageActions} + { for Action in messageActions} +
_onReply: -> @@ -129,13 +133,13 @@ MessageItem = React.createClass _renderCollapseControl: -> if @state.detailedHeaders
@setState detailedHeaders: false}>
else
@setState detailedHeaders: true}>
diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 7caf05831..eda3ea88f 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -2,7 +2,7 @@ _ = require 'underscore-plus' React = require 'react' MessageItem = require "./message-item" {Actions, ThreadStore, MessageStore, ComponentRegistry} = require("inbox-exports") -{Spinner, ResizableRegion} = require('ui-components') +{Spinner, ResizableRegion, RetinaImg} = require('ui-components') module.exports = MessageList = React.createClass @@ -39,6 +39,7 @@ MessageList = React.createClass else if didAddDraft @_focusDraft(@refs["composerItem-#{addedDraftIds[0]}"]) + @_prepareContentForDisplay() _focusDraft: (draftDOMNode) -> # We need a 100ms delay so the DOM can finish painting the elements on @@ -54,6 +55,7 @@ MessageList = React.createClass wrapClass = React.addons.classSet "messages-wrap": true + "has-reply-area": @_hasReplyArea() "ready": @state.ready
@@ -65,9 +67,36 @@ MessageList = React.createClass {@_messageListHeaders()} {@_messageComponents()}
+ {@_renderReplyArea()}
+ _renderReplyArea: -> + if @_hasReplyArea() +
+
+ Write a reply… +
+
+ else return
+ + _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. @@ -141,6 +170,9 @@ MessageList = React.createClass className={className} thread_participants={@_threadParticipants()} /> + unless idx is @state.messages.length - 1 + components.push
+ components _onChange: -> diff --git a/internal_packages/message-list/lib/message-participants.cjsx b/internal_packages/message-list/lib/message-participants.cjsx index f8cb57cd8..12c37c90a 100644 --- a/internal_packages/message-list/lib/message-participants.cjsx +++ b/internal_packages/message-list/lib/message-participants.cjsx @@ -18,7 +18,7 @@ MessageParticipants = React.createClass _renderCollapsed: -> {@_shortNames(@props.from)} -  >  + To:  {@_shortNames(@props.to)} 0 then display:"inline" else display:"none"}> Cc:  @@ -33,7 +33,6 @@ MessageParticipants = React.createClass _renderExpanded: ->
-
From: 
{@_fullContact(@props.from)}
@@ -54,11 +53,6 @@ MessageParticipants = React.createClass
{@_fullContact(@props.bcc)}
-
-
Subject: 
-
{@props.subject}
-
- _shortNames: (contacts=[]) -> diff --git a/internal_packages/message-list/spec/message-list-spec.cjsx b/internal_packages/message-list/spec/message-list-spec.cjsx index ff358513a..1822d64d7 100644 --- a/internal_packages/message-list/spec/message-list-spec.cjsx +++ b/internal_packages/message-list/spec/message-list-spec.cjsx @@ -66,93 +66,92 @@ user_5 = _.extend _.clone(user_headers), name: "User Five" email: "user5@inboxapp.com" -testMessages = [ - (new Message).fromJSON({ - "id" : "111", - "from" : [ user_1 ], - "to" : [ user_2 ], - "cc" : [ user_3, user_4 ], - "bcc" : null, - "body" : "Body One", - "date" : 1415814587, - "draft" : false - "files" : [], - "unread" : false, - "object" : "message", - "snippet" : "snippet one...", - "subject" : "Subject One", - "thread_id" : "thread_12345", - "namespace_id" : "nsid" - }), - (new Message).fromJSON({ - "id" : "222", - "from" : [ user_2 ], - "to" : [ user_1 ], - "cc" : [ user_3, user_4 ], - "bcc" : null, - "body" : "Body Two", - "date" : 1415814587, - "draft" : false - "files" : [], - "unread" : false, - "object" : "message", - "snippet" : "snippet Two...", - "subject" : "Subject Two", - "thread_id" : "thread_12345", - "namespace_id" : "nsid" - }), - (new Message).fromJSON({ - "id" : "333", - "from" : [ user_3 ], - "to" : [ user_1 ], - "cc" : [ user_2, user_4 ], - "bcc" : [], - "body" : "Body Three", - "date" : 1415814587, - "draft" : false - "files" : [], - "unread" : false, - "object" : "message", - "snippet" : "snippet Three...", - "subject" : "Subject Three", - "thread_id" : "thread_12345", - "namespace_id" : "nsid" - }), - (new Message).fromJSON({ - "id" : "444", - "from" : [ user_4 ], - "to" : [ user_1 ], - "cc" : [], - "bcc" : [ user_5 ], - "body" : "Body Four", - "date" : 1415814587, - "draft" : false - "files" : [], - "unread" : false, - "object" : "message", - "snippet" : "snippet Four...", - "subject" : "Subject Four", - "thread_id" : "thread_12345", - "namespace_id" : "nsid" - }), - (new Message).fromJSON({ - "id" : "555", - "from" : [ user_1 ], - "to" : [ user_4 ], - "cc" : [], - "bcc" : [], - "body" : "Body Five", - "date" : 1415814587, - "draft" : false - "files" : [], - "unread" : false, - "object" : "message", - "snippet" : "snippet Five...", - "subject" : "Subject Five", - "thread_id" : "thread_12345", - "namespace_id" : "nsid" - }), -] +m1 = (new Message).fromJSON({ + "id" : "111", + "from" : [ user_1 ], + "to" : [ user_2 ], + "cc" : [ user_3, user_4 ], + "bcc" : null, + "body" : "Body One", + "date" : 1415814587, + "draft" : false + "files" : [], + "unread" : false, + "object" : "message", + "snippet" : "snippet one...", + "subject" : "Subject One", + "thread_id" : "thread_12345", + "namespace_id" : "nsid" +}) +m2 = (new Message).fromJSON({ + "id" : "222", + "from" : [ user_2 ], + "to" : [ user_1 ], + "cc" : [ user_3, user_4 ], + "bcc" : null, + "body" : "Body Two", + "date" : 1415814587, + "draft" : false + "files" : [], + "unread" : false, + "object" : "message", + "snippet" : "snippet Two...", + "subject" : "Subject Two", + "thread_id" : "thread_12345", + "namespace_id" : "nsid" +}) +m3 = (new Message).fromJSON({ + "id" : "333", + "from" : [ user_3 ], + "to" : [ user_1 ], + "cc" : [ user_2, user_4 ], + "bcc" : [], + "body" : "Body Three", + "date" : 1415814587, + "draft" : false + "files" : [], + "unread" : false, + "object" : "message", + "snippet" : "snippet Three...", + "subject" : "Subject Three", + "thread_id" : "thread_12345", + "namespace_id" : "nsid" +}) +m4 = (new Message).fromJSON({ + "id" : "444", + "from" : [ user_4 ], + "to" : [ user_1 ], + "cc" : [], + "bcc" : [ user_5 ], + "body" : "Body Four", + "date" : 1415814587, + "draft" : false + "files" : [], + "unread" : false, + "object" : "message", + "snippet" : "snippet Four...", + "subject" : "Subject Four", + "thread_id" : "thread_12345", + "namespace_id" : "nsid" +}) +m5 = (new Message).fromJSON({ + "id" : "555", + "from" : [ user_1 ], + "to" : [ user_4 ], + "cc" : [], + "bcc" : [], + "body" : "Body Five", + "date" : 1415814587, + "draft" : false + "files" : [], + "unread" : false, + "object" : "message", + "snippet" : "snippet Five...", + "subject" : "Subject Five", + "thread_id" : "thread_12345", + "namespace_id" : "nsid" +}) +testMessages = [m1, m2, m3, m4, m5] draftMessages = [ (new Message).fromJSON({ "id" : "666", @@ -273,3 +272,27 @@ describe "MessageList", -> expect(items.length).toBe 1 expect(items.length).toBe 1 + + 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") + 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") + 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") + expect(cs.length).toBe 0 diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less index b86079722..b4aab81bb 100644 --- a/internal_packages/message-list/stylesheets/message-list.less +++ b/internal_packages/message-list/stylesheets/message-list.less @@ -82,6 +82,9 @@ // top:0; left:0; right:0; bottom:0; opacity:0; transition: opacity 0s; + &.has-reply-area { + padding-bottom: 63px; // height of reply footer + } } .messages-wrap.ready { opacity:1; @@ -93,37 +96,61 @@ width: 100%; margin: 0 auto; - padding: @spacing-double 0; + padding: @spacing-standard 0 @spacing-double 0; &:first-child { padding-top: 0; } - border-bottom: 1px solid @border-primary-bg; - &:last-child { border-bottom: 0; } - &.collapsed { padding: @spacing-standard @spacing-double; } } + .message-item-divider { + border-top: 2px solid @border-secondary-bg; + height: 3px; + background: @background-secondary; + border-bottom: 1px solid @border-primary-bg; + margin: 0; + margin-right: 2px; + } + .message-header { position: relative; font-size: @font-size-small; + border-bottom: 1px solid @border-color-divider; + padding-bottom: @spacing-standard; + padding-top: 5px; + + .message-actions-wrap { + width: 100%; + text-align: right; + } .message-actions { + display: inline-block; + width: 120px; + height: 22px; + border: 1px solid @border-color-divider; + border-radius: 11px; + z-index: 4; margin-top: 0.35em; margin-left: 0.5em; - text-align: right; + text-align: center; .btn-icon { + opacity: 0.75; padding: 0 @spacing-half; - &:last-child { padding-right: 0; } - margin-left: 10px; - margin-right: 0; + height: 20px; + line-height: 10px; + border-radius: 0; + border-right: 1px solid @border-color-divider; + &:last-child { border-right: 0; } + margin: 0; &:active {background: transparent;} } } - .message-time { + .message-time { z-index: 2; position: relative; display: inline-block; } @@ -141,6 +168,7 @@ .message-header-right { z-index: 4; position: relative; float: right; + text-align: right; } } @@ -149,7 +177,7 @@ width: 100%; max-width: @message-max-width; margin: 0 auto; - padding: @spacing-standard @spacing-double; + padding: 0 @spacing-double @spacing-standard @spacing-double; iframe { width: 100%; @@ -175,6 +203,34 @@ .collapse-control:hover {cursor: pointer;} + .footer-reply-area-wrap { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + color: @text-color-very-subtle; + border-top: 1px solid @border-color-divider; + background: @background-primary; + z-index: 10; + + &:hover { + cursor: default; + } + + .footer-reply-area { + width: 100%; + max-width: @message-max-width; + margin: 0 auto; + padding: 20px @spacing-double; + } + .reply-text { + display: inline-block; + margin-left: 0.5em; + position: relative; + top: 2px; + } + } + } .attachments-area { padding-top: @spacing-standard; @@ -195,8 +251,7 @@ .from-label, .to-label, .cc-label, .bcc-label, .subject-label { color: @text-color-very-subtle; } - .to-label { font-weight: 600; } - .cc-label, .bcc-label, .subject-label { margin-left: @spacing-standard; } + .to-label, .cc-label, .bcc-label, .subject-label { margin-left: @spacing-standard; } .to-contact, .cc-contact, .to-everyone { color: @text-color-very-subtle; } @@ -218,25 +273,32 @@ .from-label, .to-label, .cc-label, .bcc-label, .subject-label { float: left; display: block; - font-weight: 400; + font-weight: @font-weight-normal; margin-left: 0; } - .from-contact, .to-contact, .cc-contact, .subject { - padding-left: 4.4em; - font-weight: 500; + .from-contact, .subject { + font-weight: @font-weight-semi-bold; } - .from-label { margin-right: 1em; } - .to-label, .cc-label { margin-right: 2.15em; } - .bcc-label { margin-right: 1.7em; } + // .from-label { margin-right: 1em; } + .to-label, .cc-label { margin-right: 0.5em; } + .bcc-label { margin-right: 0; } .participant-primary { - color: @text-color; + color: @text-color-very-subtle; } .participant-secondary { color: @text-color-very-subtle; } + .from-contact { + .participant-primary { + color: @text-color; + } + .participant-secondary { + color: @text-color; + } + } } } diff --git a/static/images/message-list/forward-message-header@2x.png b/static/images/message-list/forward-message-header@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9ad3f6c389df0f25caa49f4b67c26b8728b83fbb GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^GC(ZK!3-pm?$qT0DVB6cUq=Rpjs4tz5?O(Kg=CK) zUj~LMH3o);76yi2K%s^g3=E|P3=FRl7#OT(FffQ0%-I!a1C($L@Ck7R(x#S9lpvr9 zN7&yi1yW)qL4LsuOf|>TgjdvxNb7E$5$6~!xz6&CU6d_bh9*!2W0JSK3quF1tOt<8 zS>O>_4Ai9x!i=6lDj$G?L7py-AsXk;p0ng@Fc5GK^yf*8Xa2rlYheRZfz^^fr!Hml z*cdmTnPZ&0$m2`ZN0nHHZ;(*$m8LR11~?n+w&wjqS-8g`_`g&-)$J4 XRk2*NI&w-HXgY(ZtDnm{r-UW|YLR7& literal 0 HcmV?d00001 diff --git a/static/images/message-list/reply-all-footer@2x.png b/static/images/message-list/reply-all-footer@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b73847e9b6469de27c333945fdcaf3f08a28cb1b GIT binary patch literal 894 zcmeAS@N?(olHy`uVBq!ia0vp^8bB<@!3-o@1N54J6id3JuOkD)#(wTUiL5}rLb6AY zF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=IjczVPIg?5AX?b1u8JLbQ(p( zga9zC<~4^i0jZRdAirP+R;En(NafVy;^9ew#}s2kd=GJY9Sw2yck*dwv_I@Qhr@ct zr0LSm>jW%%)$-#Cj!11V+1enpX}8k$n)2+)a$7cvsqU~`!lUlDl37J7R(J364o!po z!8&_nCCv}A?OMCWh|9`MX>&NxKE@<(cNc~ZR#^`qhqJ&VvKW{SR6&^0Gf3qFP=Sr7 zi(`n!`Pxb8(T5BK8nhS8JDS8Q!=cp_y+x}}iED?z_eJj-fBpa8T<$q}`IpS%`S-l< z#m%~tIjcv`Q_xrR;*16N?51AlXV!bUKu=}u-QE>Gbxi5cxF)$(_CxPY;n*;~zn{A^Vs18iP5r1}>`uuwL<&QF-K5M@?&m+-Uf7Vwi>U&W*FEDT!JYD@<);T3K0RY#oFyR0I literal 0 HcmV?d00001 diff --git a/static/images/message-list/reply-all-message-header@2x.png b/static/images/message-list/reply-all-message-header@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2319844f865535c05fa8ec76291cd017ca27da60 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^8bB<@!3-o@1N54J6id3JuOkD)#(wTUiL5}rLb6AY zF9SoB8UsT^3j@P1pisjL28L1t28LG&3=CE?7#PG0=IjczVPIgi4)6(a1u8JLbQ(pZ zh5#_!{I{)522vR%L4LsuoUEDicp|5Bq#mEm9X>T_QsA+^m_wGnM_p~aLXJ52J3INf z+aC_|wB9qPS=!H{S1q5hKx#v={MH60nN3RD%d;oTZOKvHAyT85roKCFC9_JhR)p@} zlF=U>`fsPR1l}cNc~ZR#^`qhqJ&VvKW{yR6&^0Gf3qF zP=ULri(`n!`PxaD;fD+aTwh1^UUL%FP~zI5pw)FnR8+*#)$z{MUHUKn|7SnPx^PfBB`0HsM{~#>( zXqhKDtwO5uv4f!S-i{-)4`^QWTwqY;=JCk)VxHjwi^{sr=?eK$8EInu2I?OtnW!?F z$UEpieHMEEyOX*lKXvFo*OzyFH`2g6ohSQ5NovsTdAzk!!5zU;ZD zC7u$;6g#oi)bpItkTuDx@APaV7V_dT{9p1jg$-KYO#;l3)9e_r$XuBqa? z-|p(RcpQ4A&G$NQW%d8>pXm;0Z6^!~$C;HNcjlJ?cDz;I^pboFyt I=akR{0KnWd+W-In literal 0 HcmV?d00001 diff --git a/static/images/message-list/reply-footer@2x.png b/static/images/message-list/reply-footer@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ef9fd1ffa8b04da9aa4130e2e9bf4e9547ad67e GIT binary patch literal 537 zcmeAS@N?(olHy`uVBq!ia0vp^>Od^Y!3-onoo8PNq*&4&eH|GXHuiJ>Nn{1`6_P!I zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC!N4G1FlSew4NziofKP}kkT$h+8axQ- z9%rjC9Ux^;666=mz$Bi!rtx@PX@j(^oQ%5A=4#!A{jGZ}Ii_ztWSzjC6&7I;J!Gcbs$f-s|Jkje+3;BrqF#}JM4 zy_Yii4=M1lJ*X<}@|*kIX4}hWYhV2TU(Q$1BrBI^&*a*>mzI1yrFm^t&Nh34 zpY=(z-X1wSZ>GUUUS<6wR~dOKw)FcRn#_2Nn{1`6_P!I zd>I(3)EF2VS{N990fib~Fff!FFfhDIU|_JC!N4G1FlSew4NziHfKP}kkT$h+8ng)L zy!bOyH9@W{3GxeOU}S1YIv&)xRB(+PFWb@8)h5}~wL@ad_w728F^@;W(VHEjv=8?ZEf?EBj7~MOups)zrKib7q}2^p3w5bLLU{G^UTu zm%gsP;D0j8bH&|)tnGbm2l@hcv&&irRNA~e@zd$4D9fSzOX`AC4MkeiKPi+8^*{QS g&U&C*tg4>DtDEQl29d9=K!-ATy85}Sb4q9e06Isj*8l(j literal 0 HcmV?d00001 diff --git a/static/workspace.less b/static/workspace.less index 9992361a9..eeb46459a 100644 --- a/static/workspace.less +++ b/static/workspace.less @@ -128,7 +128,9 @@ body.is-blurred { border-bottom: 1px solid @border-color-divider; width: 100%; height: 50px; - + + box-shadow: 0px 0px 6px rgba(0,0,0,0.09); + // prevent flexbox from ever, ever resizing toolbars, no matter // how much it thinks other content is being squished min-height: 50px; @@ -229,4 +231,4 @@ body.platform-win32 { .flexbox-handle-horizontal { cursor:ew-resize; } -} \ No newline at end of file +}