feat(*): draft icon, misc fixes, and WorkspaceStore / custom toolbar in secondary windows

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
This commit is contained in:
Ben Gotow 2015-06-03 16:02:19 -07:00
parent 9236a2289b
commit 89e9cdef8d
29 changed files with 421 additions and 286 deletions

View file

@ -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: =>
<button style={order: 101}
@ -15,4 +15,4 @@ class NewComposeButton extends React.Component
_onNewCompose: => Actions.composeNewBlankDraft()
module.exports = NewComposeButton
module.exports = ComposeButton

View file

@ -342,8 +342,6 @@ class ComposerView extends React.Component
@_saveToHistory(selections) unless source.fromUndoManager
_popoutComposer: =>
return unless @_proxy
@_proxy.changes.commit()
Actions.composePopoutDraft @props.localId
_sendDraft: (options = {}) =>

View file

@ -6,47 +6,30 @@ React = require 'react'
Message,
ComponentRegistry,
WorkspaceStore} = require('nylas-exports')
NewComposeButton = require('./new-compose-button')
ComposeButton = require('./compose-button')
ComposerView = require('./composer-view')
module.exports =
activate: (@state={}) ->
atom.registerHotWindow
windowType: "composer"
replenishNum: 2
class ComposerWithWindowProps extends React.Component
@displayName: 'ComposerWithWindowProps'
@containerRequired: false
# Register our composer as the app-wide Composer
ComponentRegistry.register ComposerView,
role: 'Composer'
constructor: (@props) ->
@state = atom.getWindowProps()
if atom.isMainWindow()
ComponentRegistry.register NewComposeButton,
location: WorkspaceStore.Location.RootSidebar.Toolbar
else
@_setupContainer()
componentDidMount: ->
@unlisten = atom.onWindowPropsReceived (windowProps) =>
{errorMessage} = windowProps
@_showInitialErrorDialog(errorMessage) if errorMessage
@setState(windowProps)
windowPropsReceived: ({draftLocalId, errorMessage}) ->
return unless @_container
React.render(
<ComposerView mode="fullwindow" localId={draftLocalId} />, @_container
)
if errorMessage
@_showInitialErrorDialog(errorMessage)
componentWillUnmount: ->
@unlisten()
deactivate: ->
ComponentRegistry.unregister(ComposerView)
if atom.isMainWindow()
ComponentRegistry.unregister(NewComposeButton)
serialize: -> @state
_setupContainer: ->
if @_container? then return # Activate once
@_container = document.createElement("div")
@_container.setAttribute("id", "composer-full-window")
@_container.setAttribute("class", "composer-full-window")
document.body.appendChild(@_container)
render: ->
<div className="composer-full-window">
<ComposerView mode="fullwindow" localId={@state.draftLocalId} />
</div>
_showInitialErrorDialog: (msg) ->
remote = require('remote')
@ -57,3 +40,30 @@ module.exports =
message: "Error"
detail: msg
}
module.exports =
activate: (@state={}) ->
atom.registerHotWindow
windowType: 'composer'
replenishNum: 2
# Register our composer as the app-wide Composer
ComponentRegistry.register ComposerView,
role: 'Composer'
if atom.isMainWindow()
ComponentRegistry.register ComposeButton,
location: WorkspaceStore.Location.RootSidebar.Toolbar
else
WorkspaceStore.defineSheet 'Main', {root: true},
list: ['Center']
ComponentRegistry.register ComposerWithWindowProps,
location: WorkspaceStore.Location.Center
deactivate: ->
atom.unregisterHotWindow('composer')
ComponentRegistry.unregister(ComposerView)
ComponentRegistry.unregister(ComposeButton)
ComponentRegistry.unregister(ComposerWithWindowProps)
serialize: -> @state

View file

@ -108,7 +108,8 @@
padding: 5px @spacing-standard 0 0;
margin: 0 @spacing-standard;
border-bottom: 1px solid @border-color-divider;
flex-shrink:0;
.subject-label {
color: @text-color-very-subtle;
float: left;
@ -280,6 +281,7 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
position: relative;
z-index: 2;
padding: 5px @spacing-standard 0 @spacing-standard;
flex-shrink: 0;
.participant {
white-space: nowrap;

View file

@ -29,7 +29,7 @@ class MessageToolbarItems extends React.Component
componentWillUnmount: =>
unsubscribe() for unsubscribe in @_unsubscribers
_onChange: => _.defer =>
_onChange: =>
@setState
threadIsSelected: FocusedContentStore.focusedId('thread')?

View file

@ -7,6 +7,8 @@ querystring = require 'querystring'
{RetinaImg} = require 'nylas-component-kit'
class ContainerView extends React.Component
@displayName: 'ContainerView'
@containerRequired: false
constructor: (@props) ->
@state = @getStateFromStore()
@ -51,10 +53,12 @@ class ContainerView extends React.Component
OnboardingActions.moveToPreviousPage()
render: =>
<ReactCSSTransitionGroup transitionName="page">
{@_pageComponent()}
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
</ReactCSSTransitionGroup>
<div className="onboarding-container">
<ReactCSSTransitionGroup transitionName="page">
{@_pageComponent()}
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
</ReactCSSTransitionGroup>
</div>
_pageComponent: =>
if @state.error

View file

@ -1,16 +1,11 @@
React = require "react"
ContainerView = require "./container-view"
remote = require "remote"
ContainerView = require './container-view'
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
module.exports =
item: null
activate: (@state) ->
# This package does nothing in other windows
return unless atom.getWindowType() is 'onboarding'
@item = document.createElement("div")
@item.setAttribute("id", "onboarding-container")
@item.setAttribute("class", "onboarding-container")
React.render(<ContainerView /> , @item)
document.body.appendChild(@item)
WorkspaceStore.defineSheet 'Main', {root: true},
list: ['Center']
ComponentRegistry.register ContainerView,
location: WorkspaceStore.Location.Center

View file

@ -59,6 +59,7 @@ class ThreadListParticipants extends React.Component
list = []
last = null
for msg in @props.thread.metadata
continue if msg.draft
from = msg.from[0]
if from and from.email isnt last
list.push({
@ -101,5 +102,4 @@ class ThreadListParticipants extends React.Component
list
module.exports = ThreadListParticipants

View file

@ -1,7 +1,7 @@
_ = require 'underscore'
React = require 'react'
classNames = require 'classnames'
{ListTabular, MultiselectList} = require 'nylas-component-kit'
{ListTabular, MultiselectList, RetinaImg} = require 'nylas-component-kit'
{timestamp, subject} = require './formatting-utils'
{Actions,
Utils,
@ -29,7 +29,7 @@ class ThreadList extends React.Component
msgs = thread.metadata
return 'unknown' unless msgs and msgs instanceof Array
msgs = _.filter msgs, (m) -> m.isSaved()
msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
msg = msgs[msgs.length - 1]
return 'unknown' unless msgs.length > 0
@ -51,7 +51,14 @@ class ThreadList extends React.Component
name: "Name"
width: 200
resolver: (thread) =>
<ThreadListParticipants thread={thread} />
hasDraft = _.find (thread.metadata ? []), (m) -> m.draft
if hasDraft
<div style={display: 'flex'}>
<ThreadListParticipants thread={thread} />
<RetinaImg name="icon-draft-pencil.png" className="draft-icon" />
</div>
else
<ThreadListParticipants thread={thread} />
c3 = new ListTabular.Column
name: "Message"

View file

@ -41,6 +41,13 @@ describe "ThreadListParticipants", ->
new Message(unread: false, from: [@ben]),
]
out: [{contact: @ben, unread: false}]
},{
name: 'single read email and draft'
in: [
new Message(unread: false, from: [@ben]),
new Message(from: [@ben], draft: true),
]
out: [{contact: @ben, unread: false}]
},{
name: 'single unread email'
in: [

View file

@ -26,6 +26,13 @@
margin-left: 1em;
}
.draft-icon {
margin-top:8px;
margin-left:10px;
flex-shrink: 0;
object-fit: contain;
}
.participants {
font-size: @font-size-small;
text-overflow: ellipsis;
@ -113,8 +120,8 @@
color: @text-color-inverse-subtle;
}
.thread-icon {
-webkit-filter: brightness(600%);
.thread-icon, .draft-icon {
-webkit-filter: brightness(600%) grayscale(100%);
}
}

View file

@ -1,5 +1,5 @@
_ = require 'underscore'
{Actions, DatabaseStore, Thread, Tag} = require 'nylas-exports'
{Actions, DatabaseStore, NamespaceStore, Thread, Tag} = require 'nylas-exports'
module.exports =
activate: ->
@ -17,9 +17,13 @@ module.exports =
incomingMessages = incoming['message'] ? []
incomingThreads = incoming['thread'] ? []
# Filter for new messages
# Filter for new messages that are not sent by the current user
myEmail = NamespaceStore.current().emailAddress
newUnread = _.filter incomingMessages, (msg) =>
msg.unread is true and msg.date?.valueOf() >= @activationTime
isUnread = msg.unread is true
isNew = msg.date?.valueOf() >= @activationTime
isFromMe = msg.from[0]?.email is myEmail
return isUnread and isNew and not isFromMe
return resolve() if newUnread.length is 0

View file

@ -4,6 +4,7 @@ Message = require '../../../src/flux/models/message'
Thread = require '../../../src/flux/models/thread'
Tag = require '../../../src/flux/models/tag'
DatabaseStore = require '../../../src/flux/stores/database-store'
NamespaceStore = require '../../../src/flux/stores/namespace-store'
Main = require '../lib/main'
describe "UnreadNotifications", ->
@ -51,6 +52,12 @@ describe "UnreadNotifications", ->
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
subject: "Hello World Old"
threadId: "A"
@msgFromMe = new Message
unread: true
date: new Date()
from: [NamespaceStore.current().me()]
subject: "A Sent Mail!"
threadId: "A"
spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
return Promise.resolve(@threadA) if id is 'A'
@ -121,3 +128,9 @@ describe "UnreadNotifications", ->
.then ->
expect(window.Notification).not.toHaveBeenCalled()
it "should not create a Notification if the new message is one I sent", ->
waitsForPromise =>
Main._onNewMailReceived({message: [@msgFromMe]})
.then ->
expect(window.Notification).not.toHaveBeenCalled()

View file

@ -5,6 +5,10 @@ class TestComponent extends React.Component
@displayName: 'TestComponent'
constructor: ->
class TestComponentNotSameIdentity extends React.Component
@displayName: 'TestComponent'
constructor: ->
class TestComponentNoDisplayName extends React.Component
constructor: ->
@ -38,9 +42,13 @@ describe 'ComponentRegistry', ->
it 'returns itself', ->
expect(ComponentRegistry.register(TestComponent, {role: "bla"})).toBe(ComponentRegistry)
it 'does not allow components to be overridden by others with the same displayName', ->
it 'does allow the exact same component to be redefined with different role/locations', ->
ComponentRegistry.register(TestComponent, {role: "bla"})
expect(-> ComponentRegistry.register(TestComponent, {role: "bla"})).toThrow()
expect(-> ComponentRegistry.register(TestComponent, {role: "other-role"})).not.toThrow()
it 'does not allow components to be overridden by other components with the same displayName', ->
ComponentRegistry.register(TestComponent, {role: "bla"})
expect(-> ComponentRegistry.register(TestComponentNotSameIdentity, {role: "bla"})).toThrow()
it 'does not allow components to be registered without a displayName', ->
expect(-> ComponentRegistry.register(TestComponentNoDisplayName, {role: "bla"})).toThrow()

View file

@ -445,18 +445,34 @@ class Atom extends Model
reload: ->
ipc.send('call-window-method', 'restart')
# Calls the `windowPropsReceived` method of all packages that are
# currently loaded
# Updates the window load settings - called when the app is ready to display
# a hot-loaded window. Causes listeners registered with `onWindowPropsReceived`
# to receive new window props.
loadSettingsChanged: (loadSettings) =>
@loadSettings = loadSettings
@constructor.loadSettings = loadSettings
{width, height, windowProps} = loadSettings
@packages.windowPropsReceived(windowProps ? {})
@emitter.emit('window-props-received', windowProps ? {})
if width and height
@setWindowDimensions({width, height})
# Public: The windowProps passed when creating the window via `newWindow`.
#
getWindowProps: ->
@getLoadSettings().windowProps ? {}
# Public: If your package declares hot-loaded window types, `onWindowPropsReceived`
# fires when your hot-loaded window is about to be shown so you can update
# components to reflect the new window props.
#
# - callback: A function to call when window props are received, just before
# the hot window is shown. The first parameter is the new windowProps.
#
onWindowPropsReceived: (callback) ->
@emitter.on('window-props-received', callback)
# Extended: Returns a {Boolean} true when the current window is maximized.
isMaximixed: ->
@getCurrentWindow().isMaximized()
@ -579,7 +595,7 @@ class Atom extends Model
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages()
@deserializeEditorWindow()
@deserializeRootWindow()
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless safeMode
@ -589,10 +605,10 @@ class Atom extends Model
'atom-workspace:add-account': =>
options =
title: 'Add an Account'
frame: false
page: 'add-account'
width: 340
height: 550
toolbar: false
resizable: false
windowType: 'onboarding'
windowPackages: ['onboarding']
@ -611,7 +627,6 @@ class Atom extends Model
{width,
height,
windowType,
windowProps,
windowPackages} = @getLoadSettings()
@loadConfig()
@ -622,10 +637,10 @@ class Atom extends Model
@packages.loadPackages(windowType)
@packages.loadPackage(pack) for pack in (windowPackages ? [])
@deserializeSheetContainer()
@packages.activate()
ipc.on("load-settings-changed", @loadSettingsChanged)
@packages.windowPropsReceived(windowProps ? {})
@keymaps.loadUserKeymap()
@ -647,6 +662,9 @@ class Atom extends Model
# src/browser/application.coffee
registerHotWindow: (options={}) -> ipc.send('register-hot-window', options)
# Unregisters a hot window with the given windowType
unregisterHotWindow: (windowType) -> ipc.send('unregister-hot-window', windowType)
unloadEditorWindow: ->
@packages.deactivatePackages()
@savedState.packageStates = @packages.packageStates
@ -737,7 +755,7 @@ class Atom extends Model
Section: Private
###
deserializeWorkspaceView: ->
deserializeSheetContainer: ->
startTime = Date.now()
# Put state back into sheet-container? Restore app state here
@deserializeTimings.workspace = Date.now() - startTime
@ -756,9 +774,9 @@ class Atom extends Model
@packages.packageStates = @savedState.packageStates ? {}
delete @savedState.packageStates
deserializeEditorWindow: ->
deserializeRootWindow: ->
@deserializePackageStates()
@deserializeWorkspaceView()
@deserializeSheetContainer()
loadThemes: ->
@themes.load()

View file

@ -294,6 +294,9 @@ class Application
ipc.on 'register-hot-window', (event, options) =>
@windowManager.registerHotWindow(options)
ipc.on 'unregister-hot-window', (event, windowType) =>
@windowManager.unregisterHotWindow(windowType)
app.on 'activate-with-no-open-windows', (event) =>
@windowManager.ensurePrimaryWindowOnscreen()
event.preventDefault()

View file

@ -18,10 +18,10 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
{frame,
title,
{title,
width,
height,
toolbar,
resizable,
pathToOpen,
hideMenuBar,
@ -36,12 +36,23 @@ class AtomWindow
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
# Mac: We'll render a CSS toolbar if `toolbar=true`. No frame required.
# Win / Linux: We don't render a toolbar in CSS - include frame if the
# window requests a toolbar. Remove this code once we have custom toolbars
# on win/linux.
toolbar ?= true
if process.platform is 'darwin'
frame = false
else
frame = toolbar
options =
show: false
title: title ? 'Nylas'
frame: frame ? true
frame: frame
#https://atomio.slack.com/archives/electron/p1432056952000608
'standard-window': frame ? true
'standard-window': frame
width: width
height: height
resizable: resizable ? true
@ -62,6 +73,7 @@ class AtomWindow
@handleEvents()
loadSettings = _.extend({}, settings)
loadSettings.toolbar = toolbar
loadSettings.windowState ?= '{}'
loadSettings.appVersion = app.getVersion()
loadSettings.resourcePath = @resourcePath

View file

@ -85,7 +85,6 @@ class WindowManager
devMode: @devMode
safeMode: @safeMode
neverClose: true
frame: process.platform isnt 'darwin'
mainWindow: true
@ -104,7 +103,7 @@ class WindowManager
newOnboardingWindow: ->
@newWindow
title: 'Welcome to Nylas'
frame: false
toolbar: false
width: 340
height: 550
resizable: false
@ -136,7 +135,10 @@ class WindowManager
#
# This means that when `newWindow` is called, instead of going through
# the bootup process, it simply replaces key parameters and does a soft
# reload via `windowPropsReceived`.
# reload.
#
# To listen for window props being sent to your existing hot-loaded window,
# add a callback to `atom.onWindowPropsChanged`.
#
# Since the window is already loaded, there are only some options that
# can be soft-reloaded. If you attempt to pass options that a soft
@ -157,10 +159,6 @@ class WindowManager
# serializable data. No functions!
# - title: The title of the page
#
# Other options that will trigger a
# - frame: defaults true. Whether or not the popup has a frame
# - forceNewWindow
#
# Other non required options:
# - All of the options of BrowserWindow
# https://github.com/atom/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions
@ -202,6 +200,28 @@ class WindowManager
@_replenishHotWindows()
unregisterHotWindow: (windowType) ->
return unless @_hotWindows[windowType]
# Remove entries from the replentishQueue
@_replenishQueue = _.reject @_replenishQueue, (item) => item.windowType is windowType
# Destroy any hot windows already loaded
destroyedLoadingWindow = false
{loadedWindows} = @_hotWindows[windowType]
for win in loadedWindows
destroyedLoadingWindow = true if not win.loaded
win.browserWindow.destroy()
# Delete the hot window configuration
delete @_hotWindows[windowType]
# If we destroyed a window that was currently loading,
# the queue will stop processing forever.
if destroyedLoadingWindow
@_processingQueue = false
@_processReplenishQueue()
# Immediately close all of the hot windows and reset the replentish queue
# to prevent more from being opened without additional calls to registerHotWindow.
#
@ -217,6 +237,7 @@ class WindowManager
@_hotWindows = {}
defaultWindowOptions: ->
#TODO: Defaults are also applied in AtomWindow.constructor.
devMode: @devMode
safeMode: @safeMode
windowType: 'popout'

View file

@ -61,8 +61,8 @@ class ComponentRegistry
throw new Error("ComponentRegistry.register() requires `role` or `location`") if not roles and not locations
if @_registry[component.displayName]
throw new Error("ComponentRegistry.register(): A component has already been registered with the name #{component.displayName}")
if @_registry[component.displayName] and @_registry[component.displayName].component isnt component
throw new Error("ComponentRegistry.register(): A different component was already registered with the name #{component.displayName}")
@_registry[component.displayName] = {component, locations, modes, roles}

View file

@ -47,8 +47,9 @@ class EventedIFrame extends React.Component
doc.removeEventListener('mousedown', @_onIFrameMouseEvent)
doc.removeEventListener('mousemove', @_onIFrameMouseEvent)
doc.removeEventListener('mouseup', @_onIFrameMouseEvent)
node.contentWindow.removeEventListener('focus', @_onIFrameFocus)
node.contentWindow.removeEventListener('blur', @_onIFrameBlur)
if node.contentWindow
node.contentWindow.removeEventListener('focus', @_onIFrameFocus)
node.contentWindow.removeEventListener('blur', @_onIFrameBlur)
_subscribeToIFrameEvents: =>
node = React.findDOMNode(@)
@ -59,8 +60,9 @@ class EventedIFrame extends React.Component
doc.addEventListener("mousedown", @_onIFrameMouseEvent)
doc.addEventListener("mousemove", @_onIFrameMouseEvent)
doc.addEventListener("mouseup", @_onIFrameMouseEvent)
node.contentWindow.addEventListener("focus", @_onIFrameFocus)
node.contentWindow.addEventListener("blur", @_onIFrameBlur)
if node.contentWindow
node.contentWindow.addEventListener("focus", @_onIFrameFocus)
node.contentWindow.addEventListener("blur", @_onIFrameBlur)
_onIFrameBlur: (event) =>
node = React.findDOMNode(@)

View file

@ -312,7 +312,9 @@ class DatabaseView extends ModelView
@log("Metadata version #{touchTime} fetched, but out of date (current is #{page.lastTouchTime})")
return
for item in items
for item, idx in items
if Object.isFrozen(item)
item = items[idx] = new @klass(item)
item.metadata = results[item.id]
page.metadata[item.id] = results[item.id]

View file

@ -310,12 +310,15 @@ class DraftStore
_onPopoutDraftLocalId: (draftLocalId, options = {}) =>
return unless NamespaceStore.current()
options.draftLocalId = draftLocalId
save = Promise.resolve()
if @_draftSessions[draftLocalId]
save = @_draftSessions[draftLocalId].changes.commit()
atom.newWindow
title: "Message"
windowType: "composer"
windowProps: options
save.then =>
atom.newWindow
title: "Message"
windowType: "composer"
windowProps: _.extend(options, {draftLocalId})
_onHandleMailtoLink: (urlString) =>
namespace = NamespaceStore.current()

View file

@ -24,21 +24,6 @@ class WorkspaceStore
@include Listener
constructor: ->
@Location = Location
@Sheet = Sheet
@defineSheet 'Global'
@defineSheet 'Threads', {root: true},
list: ['RootSidebar', 'ThreadList']
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
@defineSheet 'Drafts', {root: true, name: 'Local Drafts'},
list: ['RootSidebar', 'DraftList']
@defineSheet 'Thread', {},
list: ['MessageList', 'MessageListSidebar']
@_resetInstanceVars()
@listenTo Actions.selectRootSheet, @_onSelectRootSheet
@ -52,10 +37,24 @@ class WorkspaceStore
'application:pop-sheet': => @popSheet()
_resetInstanceVars: =>
@Location = Location = {}
@Sheet = Sheet = {}
@_preferredLayoutMode = 'list'
@_sheetStack = []
@_onSelectRootSheet(Sheet.Threads)
if atom.isMainWindow()
@defineSheet 'Global'
@defineSheet 'Threads', {root: true},
list: ['RootSidebar', 'ThreadList']
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
@defineSheet 'Drafts', {root: true, name: 'Local Drafts'},
list: ['RootSidebar', 'DraftList']
@defineSheet 'Thread', {},
list: ['MessageList', 'MessageListSidebar']
@_onSelectRootSheet(Sheet.Threads)
else
@defineSheet 'Global'
###
Inbound Events
@ -97,10 +96,13 @@ class WorkspaceStore
# Returns a {String}: The current layout mode. Either `split` or `list`
#
layoutMode: =>
if @_preferredLayoutMode in @rootSheet().supportedModes
root = @rootSheet()
if not root
'list'
else if @_preferredLayoutMode in root.supportedModes
@_preferredLayoutMode
else
@rootSheet().supportedModes[0]
root.supportedModes[0]
# Returns The top {Sheet} in the current stack. Use this method to determine
# the sheet the user is looking at.
@ -151,6 +153,9 @@ class WorkspaceStore
Header: {id: "Sheet:#{id}:Header"}
Footer: {id: "Sheet:#{id}:Footer"}
if options.root and not @rootSheet()
@_onSelectRootSheet(Sheet[id])
@triggerDebounced()
undefineSheet: (id) =>
@ -176,7 +181,7 @@ class WorkspaceStore
@_sheetStack.pop()
@trigger()
if sheet is Sheet.Thread
if Sheet.Thread and sheet is Sheet.Thread
Actions.focusInCollection(collection: 'thread', item: null)
# Return to the root sheet. This method triggers, allowing observers

View file

@ -449,6 +449,3 @@ class PackageManager
pack.deactivate()
delete @activePackages[pack.name]
@emitter.emit 'did-deactivate-package', pack
windowPropsReceived: (windowProps) ->
pack.windowPropsReceived?(windowProps) for pack in @getLoadedPackages()

View file

@ -154,9 +154,6 @@ class Package
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
windowPropsReceived: (windowProps) ->
@mainModule?.windowPropsReceived?(windowProps)
activateNow: ->
try
@activateConfig()

View file

@ -1,5 +1,6 @@
React = require 'react/addons'
Sheet = require './sheet'
Toolbar = require './sheet-toolbar'
Flexbox = require './components/flexbox'
RetinaImg = require './components/retina-img'
InjectedComponentSet = require './components/injected-component-set'
@ -10,154 +11,6 @@ _ = require 'underscore'
ComponentRegistry,
WorkspaceStore} = require "nylas-exports"
class ToolbarSpacer extends React.Component
@displayName = 'ToolbarSpacer'
@propTypes =
order: React.PropTypes.number
render: =>
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
class ToolbarBack extends React.Component
@displayName = 'ToolbarBack'
render: =>
<div className="item-back" onClick={@_onClick}>
<RetinaImg name="sheet-back.png" />
</div>
_onClick: =>
Actions.popSheet()
class ToolbarWindowControls extends React.Component
@displayName = 'ToolbarWindowControls'
render: =>
<div name="ToolbarWindowControls" className="toolbar-window-controls">
<button className="close" onClick={ -> atom.close()}></button>
<button className="minimize" onClick={ -> atom.minimize()}></button>
<button className="maximize" onClick={ -> atom.maximize()}></button>
</div>
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) =>
<div style={position: 'absolute', top:0, display:'none'}
data-column={idx}
key={idx}>
{@_flexboxForComponents(components)}
</div>
<div style={style} className={"sheet-toolbar-container mode-#{@state.mode}"}>
{toolbars}
</div>
_flexboxForComponents: (components) =>
elements = components.map (component) =>
<component key={component.displayName} {...@props} />
<TimeoutTransitionGroup
className="item-container"
component={Flexbox}
direction="row"
leaveTimeout={125}
enterTimeout={125}
transitionName="sheet-toolbar">
{elements}
<ToolbarSpacer key="spacer-50" order={-50}/>
<ToolbarSpacer key="spacer+50" order={50}/>
</TimeoutTransitionGroup>
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 <div></div> unless topSheet
sheetElements = @_sheetElements()
<Flexbox direction="column">
<div name="Toolbar" style={order:0, zIndex: 3} className="sheet-toolbar">
{toolbarElements[0]}
<TimeoutTransitionGroup leaveTimeout={125}
enterTimeout={125}
transitionName="sheet-toolbar">
{toolbarElements[1..-1]}
</TimeoutTransitionGroup>
</div>
{@_toolbarContainerElement()}
<div name="Header" style={order:1, zIndex: 2}>
<InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}
direction="column"
@ -212,6 +59,20 @@ class SheetContainer extends React.Component
</div>
</Flexbox>
_toolbarContainerElement: =>
{toolbar} = atom.getLoadSettings()
return [] unless toolbar
toolbarElements = @_toolbarElements()
<div name="Toolbar" style={order:0, zIndex: 3} className="sheet-toolbar">
{toolbarElements[0]}
<TimeoutTransitionGroup leaveTimeout={125}
enterTimeout={125}
transitionName="sheet-toolbar">
{toolbarElements[1..-1]}
</TimeoutTransitionGroup>
</div>
_toolbarElements: =>
@state.stack.map (sheet, index) ->
<Toolbar data={sheet}
@ -236,4 +97,4 @@ class SheetContainer extends React.Component
stack: WorkspaceStore.sheetStack()
module.exports = SheetContainer
module.exports = SheetContainer

159
src/sheet-toolbar.cjsx Normal file
View file

@ -0,0 +1,159 @@
React = require 'react/addons'
Sheet = require './sheet'
Flexbox = require './components/flexbox'
RetinaImg = require './components/retina-img'
TimeoutTransitionGroup = require './components/timeout-transition-group'
_ = require 'underscore'
{Actions,
ComponentRegistry,
WorkspaceStore} = require "nylas-exports"
class ToolbarSpacer extends React.Component
@displayName: 'ToolbarSpacer'
@propTypes:
order: React.PropTypes.number
render: =>
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
class ToolbarBack extends React.Component
@displayName: 'ToolbarBack'
render: =>
<div className="item-back" onClick={@_onClick}>
<RetinaImg name="sheet-back.png" />
</div>
_onClick: =>
Actions.popSheet()
class ToolbarWindowControls extends React.Component
@displayName: 'ToolbarWindowControls'
render: =>
<div name="ToolbarWindowControls" className="toolbar-window-controls">
<button className="close" onClick={ -> atom.close()}></button>
<button className="minimize" onClick={ -> atom.minimize()}></button>
<button className="maximize" onClick={ -> atom.maximize()}></button>
</div>
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) =>
<div style={position: 'absolute', top:0, display:'none'}
data-column={idx}
key={idx}>
{@_flexboxForComponents(components)}
</div>
<div style={style} className={"sheet-toolbar-container mode-#{@state.mode}"}>
{toolbars}
</div>
_flexboxForComponents: (components) =>
elements = components.map (component) =>
<component key={component.displayName} {...@props} />
<TimeoutTransitionGroup
className="item-container"
component={Flexbox}
direction="row"
leaveTimeout={125}
enterTimeout={125}
transitionName="sheet-toolbar">
{elements}
<ToolbarSpacer key="spacer-50" order={-50}/>
<ToolbarSpacer key="spacer+50" order={50}/>
</TimeoutTransitionGroup>
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -19,8 +19,8 @@ atom-workspace {
position: relative;
font-family: @font-family;
/* Important: This attribute is used in the theme-manager-specs to check that
themes load and override each other correctly. Do not remove! */
// Important: This attribute is used in the theme-manager-specs to check that
// themes load and override each other correctly. Do not remove!
background-color: @background-primary;
atom-workspace-axis.horizontal {