From 89e9cdef8dbeccfb99b61e3b4421498a7e470e1d Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 3 Jun 2015 16:02:19 -0700 Subject: [PATCH] feat(*): draft icon, misc fixes, and WorkspaceStore / custom toolbar in secondary windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Features: - ThreadListParticipants ignores drafts when computing participants, renders "Draft" label, pending design - Put the WorkspaceStore in every window—means they all get toolbars and custom gumdrop icons on Mac OS X Bug Fixes: - Never display notifications for email the user just sent - Fix obscure issue with DatabaseView trying to update metadata on items it froze. This resolves issue with names remaining bold after marking as read, drafts not appearing in message list immediately. - When you pop out a draft, save it first and *wait* for the commit() promise to succeed. - If you scroll very fast, you node.contentWindow can be null in eventedIframe Other: Make it OK to re-register the same component Make it possible to unregister a hot window Break the Sheet Toolbar out into it's own file to make things manageable Replace `package.windowPropsReceived` with a store-style model where anyone can listen for changes to `windowProps` When I put the WorkspaceStore in every window, I ran into a problem because the package was no longer rendering an instance of the Composer, it was declaring a root sheet with a composer in it. This meant that it was actually a React component that needed to listen to window props, not the package itself. `atom` is already an event emitter, so I added a `onWindowPropsReceived` hook so that components can listen to window props as if they were listening to a store. I think this might be more flexible than only broadcasting the props change event to packages. Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1592 --- ...ompose-button.cjsx => compose-button.cjsx} | 6 +- .../composer/lib/composer-view.cjsx | 2 - internal_packages/composer/lib/main.cjsx | 78 ++++---- .../composer/stylesheets/composer.less | 4 +- .../lib/message-toolbar-items.cjsx | 2 +- .../onboarding/lib/container-view.cjsx | 12 +- internal_packages/onboarding/lib/main.cjsx | 17 +- .../lib/thread-list-participants.cjsx | 2 +- .../thread-list/lib/thread-list.cjsx | 13 +- .../spec/thread-list-participants-spec.cjsx | 7 + .../thread-list/stylesheets/thread-list.less | 11 +- .../unread-notifications/lib/main.coffee | 10 +- .../spec/main-spec.coffee | 13 ++ spec-nylas/component-registry-spec.coffee | 12 +- src/atom.coffee | 38 +++- src/browser/application.coffee | 3 + src/browser/atom-window.coffee | 20 +- src/browser/window-manager.coffee | 35 +++- src/component-registry.coffee | 4 +- src/components/evented-iframe.cjsx | 10 +- src/flux/stores/database-view.coffee | 4 +- src/flux/stores/draft-store.coffee | 13 +- src/flux/stores/workspace-store.coffee | 43 +++-- src/package-manager.coffee | 3 - src/package.coffee | 3 - src/sheet-container.cjsx | 179 ++---------------- src/sheet-toolbar.cjsx | 159 ++++++++++++++++ .../thread-list/icon-draft-pencil@2x.png | Bin 0 -> 15371 bytes static/workspace.less | 4 +- 29 files changed, 421 insertions(+), 286 deletions(-) rename internal_packages/composer/lib/{new-compose-button.cjsx => compose-button.cjsx} (78%) create mode 100644 src/sheet-toolbar.cjsx create mode 100644 static/images/thread-list/icon-draft-pencil@2x.png diff --git a/internal_packages/composer/lib/new-compose-button.cjsx b/internal_packages/composer/lib/compose-button.cjsx similarity index 78% rename from internal_packages/composer/lib/new-compose-button.cjsx rename to internal_packages/composer/lib/compose-button.cjsx index 38164e51e..e63d22693 100644 --- a/internal_packages/composer/lib/new-compose-button.cjsx +++ b/internal_packages/composer/lib/compose-button.cjsx @@ -2,8 +2,8 @@ React = require 'react' {Message, Actions, NamespaceStore} = require 'nylas-exports' {RetinaImg} = require 'nylas-component-kit' -class NewComposeButton extends React.Component - @displayName: 'NewComposeButton' +class ComposeButton extends React.Component + @displayName: 'ComposeButton' render: => - - - - -ComponentRegistry.register ToolbarWindowControls, - location: WorkspaceStore.Sheet.Global.Toolbar.Left - -class Toolbar extends React.Component - displayName = 'Toolbar' - - propTypes = - data: React.PropTypes.object - depth: React.PropTypes.number - - constructor: (@props) -> - @state = @_getStateFromStores() - - componentDidMount: => - @unlisteners = [] - @unlisteners.push WorkspaceStore.listen (event) => - @setState(@_getStateFromStores()) - @unlisteners.push ComponentRegistry.listen (event) => - @setState(@_getStateFromStores()) - window.addEventListener("resize", @_onWindowResize) - window.requestAnimationFrame => @recomputeLayout() - - componentWillUnmount: => - window.removeEventListener("resize", @_onWindowResize) - unlistener() for unlistener in @unlisteners - - componentWillReceiveProps: (props) => - @setState(@_getStateFromStores(props)) - - componentDidUpdate: => - # Wait for other components that are dirty (the actual columns in the sheet) - # to update as well. - window.requestAnimationFrame => @recomputeLayout() - - shouldComponentUpdate: (nextProps, nextState) => - # This is very important. Because toolbar uses ReactCSSTransitionGroup, - # repetitive unnecessary updates can break animations and cause performance issues. - not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state) - - render: => - style = - position:'absolute' - width:'100%' - height:'100%' - zIndex: 1 - - toolbars = @state.columns.map (components, idx) => -
- {@_flexboxForComponents(components)} -
- -
- {toolbars} -
- - _flexboxForComponents: (components) => - elements = components.map (component) => - - - - {elements} - - - - - recomputeLayout: => - # Find our item containers that are tied to specific columns - columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]') - - # Find the top sheet in the stack - sheet = document.querySelectorAll("[name='Sheet']")[@props.depth] - return unless sheet - - # Position item containers so they have the position and width - # as their respective columns in the top sheet - for columnToolbarEl in columnToolbarEls - column = columnToolbarEl.dataset.column - columnEl = sheet.querySelector("[data-column='#{column}']") - continue unless columnEl - - columnToolbarEl.style.display = 'inherit' - columnToolbarEl.style.left = "#{columnEl.offsetLeft}px" - columnToolbarEl.style.width = "#{columnEl.offsetWidth}px" - - _onWindowResize: => - @recomputeLayout() - - _getStateFromStores: (props) => - props ?= @props - state = - mode: WorkspaceStore.layoutMode() - columns: [] - - # Add items registered to Regions in the current sheet - if @props.data?.columns[state.mode]? - for loc in @props.data.columns[state.mode] - entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode}) - state.columns.push(entries) - - # Add left items registered to the Sheet instead of to a Region - for loc in [WorkspaceStore.Sheet.Global, @props.data] - entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode}) - state.columns[0]?.push(entries...) - state.columns[0]?.push(ToolbarBack) if @props.depth > 0 - - # Add right items registered to the Sheet instead of to a Region - for loc in [WorkspaceStore.Sheet.Global, @props.data] - entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode}) - state.columns[state.columns.length - 1]?.push(entries...) - - state - - class SheetContainer extends React.Component displayName = 'SheetContainer' @@ -177,19 +30,13 @@ class SheetContainer extends React.Component totalSheets = @state.stack.length topSheet = @state.stack[totalSheets - 1] - toolbarElements = @_toolbarElements() + return
unless topSheet + sheetElements = @_sheetElements() -
- {toolbarElements[0]} - - {toolbarElements[1..-1]} - -
- + {@_toolbarContainerElement()} +
+ _toolbarContainerElement: => + {toolbar} = atom.getLoadSettings() + return [] unless toolbar + + toolbarElements = @_toolbarElements() +
+ {toolbarElements[0]} + + {toolbarElements[1..-1]} + +
+ _toolbarElements: => @state.stack.map (sheet, index) -> +
+ +class ToolbarBack extends React.Component + @displayName: 'ToolbarBack' + render: => +
+ +
+ + _onClick: => + Actions.popSheet() + +class ToolbarWindowControls extends React.Component + @displayName: 'ToolbarWindowControls' + render: => +
+ + + +
+ +ComponentRegistry.register ToolbarWindowControls, + location: WorkspaceStore.Sheet.Global.Toolbar.Left + +class Toolbar extends React.Component + @displayName: 'Toolbar' + + @propTypes: + data: React.PropTypes.object + depth: React.PropTypes.number + + constructor: (@props) -> + @state = @_getStateFromStores() + + componentDidMount: => + @unlisteners = [] + @unlisteners.push WorkspaceStore.listen (event) => + @setState(@_getStateFromStores()) + @unlisteners.push ComponentRegistry.listen (event) => + @setState(@_getStateFromStores()) + window.addEventListener("resize", @_onWindowResize) + window.requestAnimationFrame => @recomputeLayout() + + componentWillUnmount: => + window.removeEventListener("resize", @_onWindowResize) + unlistener() for unlistener in @unlisteners + + componentWillReceiveProps: (props) => + @setState(@_getStateFromStores(props)) + + componentDidUpdate: => + # Wait for other components that are dirty (the actual columns in the sheet) + # to update as well. + window.requestAnimationFrame => @recomputeLayout() + + shouldComponentUpdate: (nextProps, nextState) => + # This is very important. Because toolbar uses ReactCSSTransitionGroup, + # repetitive unnecessary updates can break animations and cause performance issues. + not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state) + + render: => + style = + position:'absolute' + width:'100%' + height:'100%' + zIndex: 1 + + toolbars = @state.columns.map (components, idx) => +
+ {@_flexboxForComponents(components)} +
+ +
+ {toolbars} +
+ + _flexboxForComponents: (components) => + elements = components.map (component) => + + + + {elements} + + + + + recomputeLayout: => + # Find our item containers that are tied to specific columns + columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]') + + # Find the top sheet in the stack + sheet = document.querySelectorAll("[name='Sheet']")[@props.depth] + return unless sheet + + # Position item containers so they have the position and width + # as their respective columns in the top sheet + for columnToolbarEl in columnToolbarEls + column = columnToolbarEl.dataset.column + columnEl = sheet.querySelector("[data-column='#{column}']") + continue unless columnEl + + columnToolbarEl.style.display = 'inherit' + columnToolbarEl.style.left = "#{columnEl.offsetLeft}px" + columnToolbarEl.style.width = "#{columnEl.offsetWidth}px" + + _onWindowResize: => + @recomputeLayout() + + _getStateFromStores: (props) => + props ?= @props + state = + mode: WorkspaceStore.layoutMode() + columns: [] + + # Add items registered to Regions in the current sheet + if @props.data?.columns[state.mode]? + for loc in @props.data.columns[state.mode] + entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode}) + state.columns.push(entries) + + # Add left items registered to the Sheet instead of to a Region + for loc in [WorkspaceStore.Sheet.Global, @props.data] + entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode}) + state.columns[0]?.push(entries...) + state.columns[0]?.push(ToolbarBack) if @props.depth > 0 + + # Add right items registered to the Sheet instead of to a Region + for loc in [WorkspaceStore.Sheet.Global, @props.data] + entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode}) + state.columns[state.columns.length - 1]?.push(entries...) + + state + +module.exports = Toolbar \ No newline at end of file diff --git a/static/images/thread-list/icon-draft-pencil@2x.png b/static/images/thread-list/icon-draft-pencil@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6cc2dae79a4a48225eea24b23fe5a67d38352202 GIT binary patch literal 15371 zcmeI3dsGv57RN_HsZm6SS(tEGv(kEoQY=&>EDPr3XzJ1VihWvp<*fA z_2O%Jw54{MEoicSW>-4;mxiBBQ4SPA^78UTc`^~{%t9n8l?o9{5vf#2D}=6mJB7K0 zc9*~#q&totcbS}ogCa;f%*4eq$y`dq=QD|XudeH|Iedxit}b>oMZ}Fc5Q#{P^yg$Y z`FtF?POE2WW)p&2aT{)@T(qC0Uq44SNs+E>a)6Y+=>ZedH8B``x%*wO&DL*f7ZtmN zM$m<{Ur(1Y-+?2^xQonnn(){q^rZ;;yLyVS^c1I8BxYv7wc~DL05@jF>*i?%Z-vzK zwW2yFj!~r3NRn2qx66{c5yCp1r>o#e2Fyg*8LpGnh`&=$#e3r6dW^!gbVtgCVuesF zGfE_MJED>35N=w+VEF5vzv(!k>AfWX16{;hxq?2A2u9c~g{)PcBPp_c~b{B=&O?ZM{OAm+$ z!i;8`F|pZXQ3@4il~fps%M`*WxmYI5#8nnWq+Di+v_yFz8VsO&pU`^Jl*{awuF&Q` z7#eWz6MBx5pwBwY3TT+s|Irlvgz1wio^a6}nh$g?-M^mGY##nb@6qlFE8#hn9GKIE zGkZ?M@7{F2W6XC-F+QHXi()3`Jky$(ErFZWNUzbJW%)e2+=&0TfZq6k`@lK!vT^&< zHk3D%M>TK|7ippLFee_9Mfb$-+wI=Cy&e4%s1fELz)JKkg?F@fg}QsTZ$;J2!G_Y^ zK(7q*R6(Lf`i%8t_Ucoas#=XKfWrmSV6{11tcXAXhYO^^YIC?)5rF~@ z7f6HE=5Vnh0tFl{kOr&G;bKJu3OHOK4OW}O#fk_NaJWDktTu;>6%i=laDg;fZ4MVJ zB2d8L0%@??94=Nwpn$^#(qOeYT&##d0f!5u!D@53SP_8&4i`v+)qW~2UeDuyxSf9H zFOPn>Pm(2+(GLc~rubw71QomtK`Toj=;0Ik`vwH%iXrG`1_Ys-At;P2U37dV1O>dF zppP-SFMq%3T+Vp>`IFJ-%y}C`rGb{F>qGUew?0ZvK59LM%t{HScB+NtOy zUfr|**qzR^h0F-Kv@$H@z~=e?OqkkeA6Yr~sIKPizm~32j9Gl~;GvrKPWLxk4!*z7 z@cFdMCAV*UkZ<{+qxM{fwmB<(ZpyZn3ik;?Aj~f+bhqznZ~w*NPS$r8b4>38r!+8dH0DkP+joQmYpBgE;Sx`Ffwpk@wze7)|8DnAJOm$FK&c$aB7@8 zdc(T#y@lHI&zQG<6H|HctGE~?vseCn<>PWnP&)3>jrJFV8;Xv9 zS2^g@vWV?-ua7)0D9@U5Az5B~L7V>LG1ar9Rw%zT!ZY48TrOFB_qpuxoA;!>6ubFn z-J-P{C+6kuGe8m7B5(mJDf`Rg11(d&nAjGx9w`!KJZjoH?crGCrqOF#e<+U$J+%Yf z5uYIJc+~po{%<#~YMtw7u0OuE?AJB*ie*dN(n=<3-Wk;v7!)7Ii`{u<>86>n&EN3~ z%QX(cbI}!JY9~hBH4T2HXv&w<1fMsF4qOcmdvvQ(ap~hZfnl1Kqpz&)Xs1rMxj#HN z;j?uMhwk7n8~@U>lZMogptB(@S0@+Q3dP?Ve~l^+R|MQwVyy}De~WsMIc3GHl)uSq z?w$My{^{7nsiwxrLCEm0uQWD zYs2Wr`npbD(AXC{v(Mj&+dVyM@2D8n>wAA%kr@2Kwe`ijjQsf*?oGLqG3m!8%QwW| zT6{kD(&Wld!b*zC#F#B#MO>p|r(aw(L$WV)+}^j3Rl~2<3y16&;l8L@@dZ8nMowbv z_l?`Hzk|YG9jhg3`=@g{a|%|KIjeZojkX-LxU4ajEkKr)?V-j;qhTUsKqY{dIc9(&^E>eA}A p{C4-+<0t>NPI}UL@osYfbOpbuKQSz{k$GG