feat(preferences): WIP Preferences panel

Summary:
Things still to come:
- General tab
- Signatures tab (potentially remove and land)

Adding emacs things to gitignore

Adding progress. iterating on html/css is incredibly painful

Added layout for accounts page.

Adding layout for appearance page

layout for shortcuts preferences page

adding layount for notifications menu

Adding signatures layout

WIP

WIP - tab switching, accounts tab

WIP ALL THE THINGS

Keymap template support (Gmail / outlook, etc.)

Test Plan: No tests atm

Reviewers: evan

Differential Revision: https://phab.nylas.com/D1890
This commit is contained in:
Ben Gotow 2015-08-14 15:40:11 -07:00
parent 28369dc215
commit 79b6c1d94a
67 changed files with 1659 additions and 116 deletions

6
.gitignore vendored
View file

@ -15,4 +15,8 @@ docs/includes
spec/fixtures/evil-files/
yoursway-create-dmg
!spec-nylas/fixtures/packages/package-with-incompatible-native-module/node_modules
!spec-nylas/fixtures/packages/package-with-incompatible-native-module/node_modules
#emacs
*~
*#

View file

@ -7,6 +7,7 @@ Utils = require '../src/flux/models/utils'
Exports =
React: require 'react'
ReactRemote: require '../src/react-remote/react-remote-parent'
BufferedProcess: require '../src/buffered-process'
BufferedNodeProcess: require '../src/buffered-node-process'
@ -90,6 +91,8 @@ Exports =
QuotedPlainTextParser: require '../src/services/quoted-plain-text-parser'
QuotedHTMLParser: require '../src/services/quoted-html-parser'
LaunchServices: require '../src/launch-services'
# Also include all of the model classes
for key, klass of Utils.modelClassMap()
Exports[klass.name] = klass

View file

@ -242,8 +242,12 @@
// Overrides for the composer in a message-list
#message-list {
.message-item-wrap {
.message-item-white-wrap.composer-outer-wrap {
background: mix(@background-primary, #ffbb00, 96%);
}
.message-item-white-wrap.composer-outer-wrap.focused {
box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08), 0 0 3px @accent-primary;
background-color: @background-primary;
}
}
}

View file

@ -68,6 +68,9 @@ class DeveloperBar extends React.Component
<div className="btn-container pull-right">
<div className="btn" onClick={@_onToggleRegions}>Component Regions</div>
</div>
<div className="btn-container pull-right">
<div className="btn" onClick={@_onToggleReactRemoteContainer}>React Remote Container</div>
</div>
</div>
{@_sectionContent()}
<div className="footer">
@ -159,6 +162,10 @@ class DeveloperBar extends React.Component
_onToggleRegions: =>
Actions.toggleComponentRegions()
_onToggleReactRemoteContainer: =>
{ReactRemote} = require('nylas-exports')
ReactRemote.toggleContainerVisible()
_getStateFromStores: =>
visible: DeveloperBarStore.visible()
queue: TaskQueue._queue

View file

@ -12,27 +12,39 @@ class MessageControls extends React.Component
constructor: (@props) ->
render: =>
items = @_items()
<div className="message-actions-wrap">
<ButtonDropdown
primaryItem={<RetinaImg name="ic-message-button-reply.png" mode={RetinaImg.Mode.ContentIsMask}/>}
primaryClick={@_onReply}
menu={@_dropdownMenu()}/>
primaryItem={<RetinaImg name={items[0].image} mode={RetinaImg.Mode.ContentIsMask}/>}
primaryClick={items[0].select}
menu={@_dropdownMenu(items[1..-1])}/>
<div className="message-actions-ellipsis" onClick={@_onShowActionsMenu}>
<RetinaImg name={"message-actions-ellipsis.png"} mode={RetinaImg.Mode.ContentIsMask}/>
</div>
</div>
_dropdownMenu: ->
items = [{
_items: ->
reply =
name: 'Reply',
image: 'ic-dropdown-reply.png'
select: @_onReply
replyAll =
name: 'Reply All',
image: 'ic-dropdown-replyall.png'
select: @_onReplyAll
},{
forward =
name: 'Forward',
image: 'ic-dropdown-forward.png'
select: @_onForward
}]
defaultReplyType = atom.config.get('core.sending.defaultReplyType')
if @props.message.canReplyAll() and defaultReplyType is 'reply-all'
return [replyAll, reply, forward]
else
return [reply, replyAll, forward]
_dropdownMenu: (items) ->
itemContent = (item) ->
<span>
<RetinaImg name={item.image} mode={RetinaImg.Mode.ContentIsMask}/>

View file

@ -264,13 +264,16 @@ class MessageList extends React.Component
</div>
</div>
# Either returns "reply" or "reply-all"
# Returns either "reply" or "reply-all"
_replyType: =>
defaultReplyType = atom.config.get('core.sending.defaultReplyType')
lastMsg = _.last(_.filter((@state.messages ? []), (m) -> not m.draft))
if lastMsg?.cc.length is 0 and lastMsg?.to.length is 1
return "reply"
return 'reply' unless lastMsg
if lastMsg.canReplyAll() and defaultReplyType is 'reply-all'
return 'reply-all'
else
return "reply-all"
return 'reply'
_onRemoveLabel: (label) =>
task = new ChangeLabelsTask(thread: @state.currentThread, labelsToRemove: [label])

View file

@ -211,9 +211,9 @@ describe "MessageList", ->
MessageParticipants)
expect(items.length).toBe 1
it "toggles star on a thread if 's' is pressed", ->
it "toggles star on a thread if 'core:star-item' is fired", ->
spyOn(@messageList, "_onStar")
NylasTestUtils.keyPress("s", document.body)
atom.keymaps.dispatchCommandEvent('core:star-item', document.body, new KeyboardEvent('keydown'))
expect(@messageList._onStar).toHaveBeenCalled()
it "focuses new composers when a draft is added", ->

View file

@ -12,10 +12,12 @@ class ModeToggle extends React.Component
@state = @_getStateFromStores()
componentDidMount: =>
@unsubscribe = WorkspaceStore.listen(@_onStateChanged, @)
@_unsubscriber = WorkspaceStore.listen(@_onStateChanged)
@_mounted = true
componentWillUnmount: =>
@unsubscribe?()
@_mounted = false
@_unsubscriber?()
render: =>
return <div></div> unless @state.visible
@ -30,20 +32,29 @@ class ModeToggle extends React.Component
</button>
_onStateChanged: =>
# We need to keep track of this because our parent unmounts us in the same
# event listener cycle that we receive the event in. ie:
#
# for listener in listeners
# # 1. workspaceView remove left column
# # ---- Mode toggle unmounts, listeners array mutated in place
# # 2. ModeToggle update
return unless @_mounted
@setState(@_getStateFromStores())
_getStateFromStores: =>
rootModes = WorkspaceStore.rootSheet().supportedModes
rootVisible = WorkspaceStore.rootSheet() is WorkspaceStore.topSheet()
mode: WorkspaceStore.layoutMode()
mode: WorkspaceStore.preferredLayoutMode()
visible: rootVisible and rootModes and rootModes.length > 1
_onToggleMode: =>
if @state.mode is 'list'
Actions.selectLayoutMode('split')
atom.config.set('core.workspace.mode', 'split')
else
Actions.selectLayoutMode('list')
atom.config.set('core.workspace.mode', 'list')
return
module.exports = ModeToggle

View file

@ -1,5 +1,4 @@
{Actions} = require 'nylas-exports'
LaunchServices = require './launch-services'
{Actions, LaunchServices} = require 'nylas-exports'
NOTIF_ACTION_YES = 'mailto:set-default-yes'
NOTIF_ACTION_NO = 'mailto:set-default-no'

View file

@ -0,0 +1,57 @@
module.exports =
activate: (@state={}) ->
ipc = require 'ipc'
React = require 'react'
Preferences = require('./preferences')
{ReactRemote,
Actions} = require('nylas-exports')
Actions.registerPreferencesTab({
icon: 'ic-settings-general.png'
name: 'General'
component: require './tabs/preferences-general'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-accounts.png'
name: 'Accounts'
component: require './tabs/preferences-accounts'
})
# Actions.registerPreferencesTab({
# icon: 'ic-settings-mailrules.png'
# name: 'Mail Rules'
# component: require './tabs/preferences-mailrules'
# })
Actions.registerPreferencesTab({
icon: 'ic-settings-shortcuts.png'
name: 'Shortcuts'
component: require './tabs/preferences-keymaps'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-notifications.png'
name: 'Notifications'
component: require './tabs/preferences-notifications'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-appearance.png'
name: 'Appearance'
component: require './tabs/preferences-appearance'
})
# Actions.registerPreferencesTab({
# icon: 'ic-settings-signatures.png'
# name: 'Signatures'
# component: require './tabs/preferences-signatures'
# })
ipc.on 'open-preferences', (detail) ->
ReactRemote.openWindowForComponent(Preferences, {
tag: 'preferences'
width: 520
height: 400
resizable: false
stylesheetRegex: /preferences/
})
deactivate: ->
serialize: -> @state

View file

@ -0,0 +1,34 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesHeader extends React.Component
@displayName: 'PreferencesHeader'
@propTypes:
tabs: React.PropTypes.array.isRequired
changeActiveTab: React.PropTypes.func.isRequired
activeTab: React.PropTypes.object
render: =>
<Flexbox className="preference-header" direction="row" style={alignItems: "center"}>
{ @props.tabs.map (tab) =>
classname = "preference-header-item"
classname += " active" if tab is @props.activeTab
<div className={classname} onClick={ => @props.changeActiveTab(tab) } key={tab.name}>
<div className="phi-container">
<div className="icon">
<RetinaImg mode={RetinaImg.Mode.ContentPreserve} name={tab.icon} />
</div>
<div className="name">
{tab.name}
</div>
</div>
</div>
}
<div key="space" className="preference-header-item-spacer"></div>
</Flexbox>
module.exports = PreferencesHeader

View file

@ -0,0 +1,19 @@
Reflux = require 'reflux'
_ = require 'underscore'
NylasStore = require 'nylas-store'
{Actions} = require 'nylas-exports'
class PreferencesStore extends NylasStore
constructor: ->
@_tabs = []
@listenTo Actions.registerPreferencesTab, @_registerTab
tabs: =>
@_tabs
_registerTab: (tabConfig) =>
@_tabs.push(tabConfig)
@_triggerSoon ?= _.debounce(( => @trigger()), 20)
@_triggerSoon()
module.exports = new PreferencesStore()

View file

@ -0,0 +1,73 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
PreferencesStore = require './preferences-store'
PreferencesHeader = require './preferences-header'
class Preferences extends React.Component
@displayName: 'Preferences'
constructor: (@props) ->
@state = _.extend @getStateFromStores(),
activeTab: PreferencesStore.tabs()[0]
componentDidMount: =>
@unlisteners = []
@unlisteners.push PreferencesStore.listen =>
@setState(@getStateFromStores())
@unlisteners.push atom.config.observe null, (val) =>
@setState(@getStateFromStores())
componentWillUnmount: =>
unlisten() for unlisten in @unlisteners
componentDidUpdate: =>
if @state.tabs.length > 0 and not @state.activeTab
@setState(activeTab: @state.tabs[0])
getStateFromStores: =>
config: @getConfigWithMutators()
tabs: PreferencesStore.tabs()
getConfigWithMutators: =>
_.extend atom.config.get(), {
get: (key) =>
atom.config.get(key)
set: (key, value) =>
atom.config.set(key, value)
return
toggle: (key) =>
atom.config.set(key, !atom.config.get(key))
return
contains: (key, val) =>
vals = atom.config.get(key)
return false unless vals and vals instanceof Array
return val in vals
toggleContains: (key, val) =>
vals = atom.config.get(key)
vals = [] unless vals and vals instanceof Array
if val in vals
atom.config.set(key, _.without(vals, val))
else
atom.config.set(key, vals.concat([val]))
return
}
render: =>
if @state.activeTab
bodyElement = <@state.activeTab.component config={@state.config} />
else
bodyElement = <div>No Tab Active</div>
<div className="preferences-wrap">
<PreferencesHeader tabs={@state.tabs}
activeTab={@state.activeTab}
changeActiveTab={@_onChangeActiveTab}/>
{bodyElement}
</div>
_onChangeActiveTab: (tab) =>
@setState(activeTab: tab)
module.exports = Preferences

View file

@ -0,0 +1,99 @@
React = require 'react'
_ = require 'underscore'
{NamespaceStore} = require 'nylas-exports'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesAccounts extends React.Component
@displayName: 'PreferencesAccounts'
@propTypes:
config: React.PropTypes.object.isRequired
constructor: (@props) ->
@state = @getStateFromStores()
componentDidMount: =>
@unsubscribe = NamespaceStore.listen @_onNamespaceChange
componentWillUnmount: =>
@unsubscribe?()
render: =>
<div className="container-accounts">
<div className="section-header">
Account:
</div>
<div className="well large">
{@_renderNamespace()}
</div>
{@_renderLinkedAccounts()}
</div>
_renderNamespace: =>
return false unless @state.namespace
<Flexbox direction="row" style={alignItems: 'middle'}>
<div style={textAlign: "center"}>
<RetinaImg name={"ic-settings-account-#{@state.namespace.provider}.png"}
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
<div style={flex: 1, marginLeft: 10}>
<div className="account-name">{@state.namespace.emailAddress}</div>
<div className="account-subtext">{@state.namespace.name || "No name provided."} ({@state.namespace.displayProvider()})</div>
</div>
<div style={textAlign:"right"}>
<button className="btn btn-larger" onClick={@_onLogout}>Log out</button>
</div>
</Flexbox>
_renderLinkedAccounts: =>
accounts = @getLinkedAccounts()
return false unless accounts.length > 0
<div>
<div className="section-header">
Linked Accounts:
</div>
{ accounts.map (name) =>
<div className="well small" key={name}>
{@_renderLinkedAccount(name)}
</div>
}
</div>
_renderLinkedAccount: (name) =>
<Flexbox direction="row" style={alignItems: "center"}>
<div>
<RetinaImg name={"ic-settings-account-#{name}.png"} fallback="ic-settings-account-imap.png" />
</div>
<div style={flex: 1, marginLeft: 10}>
<div className="account-name">{name}</div>
</div>
<div style={textAlign:"right"}>
<button onClick={ => @_onUnlinkAccount(name) } className="btn btn-large">Unlink</button>
</div>
</Flexbox>
getStateFromStores: =>
namespace: NamespaceStore.current()
getLinkedAccounts: =>
return [] unless @props.config
accounts = []
for key in ['salesforce']
accounts.push(key) if @props.config[key]
accounts
_onNamespaceChange: =>
@setState(@getStateFromStores())
_onUnlinkAccount: (name) =>
atom.config.unset(name)
return
_onLogout: =>
atom.logout()
module.exports = PreferencesAccounts

View file

@ -0,0 +1,62 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class AppearanceModeOption extends React.Component
@propTypes:
mode: React.PropTypes.string.isRequired
active: React.PropTypes.bool
onClick: React.PropTypes.func
constructor: (@props) ->
render: =>
classname = "appearance-mode"
classname += " active" if @props.active
<div className={classname} onClick={@props.onClick}>
<RetinaImg name={"appearance-mode-#{@props.mode}.png"} mode={RetinaImg.Mode.ContentIsMask}/>
<div>{@props.mode} View</div>
</div>
class PreferencesAppearance extends React.Component
@displayName: 'PreferencesAppearance'
@propTypes:
config: React.PropTypes.object
render: =>
<div className="container-appearance">
<div className="section">
<div className="section-header">
Layout and theme:
</div>
<div className="section-body section-appearance">
<Flexbox direction="row" style={alignItems: "center"}>
{['list', 'split'].map (mode) =>
<AppearanceModeOption
mode={mode} key={mode}
active={@props.config.get('core.workspace.mode') is mode}
onClick={ => @props.config.set('core.workspace.mode', mode)} />
}
</Flexbox>
<div className="section-header">
<input type="checkbox"
id="dark"
checked={@props.config.contains('core.themes','ui-dark')}
onChange={ => @props.config.toggleContains('core.themes', 'ui-dark')}
/>
<label htmlFor="dark">Use dark color scheme</label>
</div>
<div className="section-header">
Set font size:
<select>
<option>12</option>
</select>
Points
</div>
</div>
</div>
</div>
module.exports = PreferencesAppearance

View file

@ -0,0 +1,72 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
{LaunchServices} = require 'nylas-exports'
class PreferencesGeneral extends React.Component
@displayName: 'PreferencesGeneral'
constructor: (@props) ->
@state = {}
@_services = new LaunchServices()
if @_services.available()
@_services.isRegisteredForURLScheme 'mailto', (registered) =>
@setState(defaultClient: registered)
toggleDefaultMailClient: =>
if @state.defaultClient is true
@setState(defaultClient: false)
@_services.resetURLScheme('mailto')
else
@setState(defaultClient: true)
@_services.registerForURLScheme('mailto')
render: =>
<div className="container-notifications">
<div className="section">
<div className="section-header platform-darwin-only" style={marginBottom:30}>
<input type="checkbox" id="default-client" checked={@state.defaultClient} onChange={@toggleDefaultMailClient}/>
<label htmlFor="default-client">Use Nylas as my default mail client</label>
</div>
<div className="section-header">
Delay for marking messages as read:
<select value={@props.config.get('core.reading.markAsReadDelay')}
onChange={ (event) => @props.config.set('core.reading.markAsReadDelay', event.target.value) }>
<option value={0}>Instantly</option>
<option value={500}>½ Second</option>
<option value={2000}>2 Seconds</option>
</select>
</div>
<div className="section-header">
Download attachments for new mail:
<select value={@props.config.get('core.attachments.downloadPolicy')}
onChange={ (event) => @props.config.set('core.attachments.downloadPolicy', event.target.value) }>
<option value="on-receive">When Received</option>
<option value="on-read">When Reading</option>
<option value="manually">Manually</option>
</select>
</div>
<div className="section-header">
Default reply behavior:
<div style={float:'right', width:138}>
<input type="radio"
id="core.sending.defaultReplyType.reply"
checked={@props.config.get('core.sending.defaultReplyType') == 'reply'}
onChange={ => @props.config.set('core.sending.defaultReplyType', 'reply') }/>
<label htmlFor="core.sending.defaultReplyType.reply">Reply</label>
<br/>
<input type="radio"
id="core.sending.defaultReplyType.replyAll"
checked={@props.config.get('core.sending.defaultReplyType') == 'reply-all'}
onChange={ => @props.config.set('core.sending.defaultReplyType', 'reply-all') }/>
<label htmlFor="core.sending.defaultReplyType.replyAll">Reply all</label>
</div>
</div>
</div>
</div>
module.exports = PreferencesGeneral

View file

@ -0,0 +1,88 @@
React = require 'react'
_ = require 'underscore'
path = require 'path'
fs = require 'fs'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
DisplayedKeybindings = [
['application:new-message', 'New Message'],
['application:reply', 'Reply'],
['application:reply-all', 'Reply All'],
['application:forward', 'Forward'],
['application:focus-search', 'Search'],
['application:change-category', 'Change Folder / Labels'],
['core:select-item', 'Select Focused Item'],
['core:star-item', 'Star Focused Item'],
]
class PreferencesKeymaps extends React.Component
@displayName: 'PreferencesKeymaps'
constructor: (@props) ->
@state =
templates: []
bindings: @_getStateFromKeymaps()
@_loadTemplates()
componentDidMount: =>
@unsubscribe = atom.keymaps.onDidReloadKeymap =>
@setState(bindings: @_getStateFromKeymaps())
componentWillUnmount: =>
@unsubscribe?()
_loadTemplates: =>
templatesDir = path.join(atom.getLoadSettings().resourcePath, 'keymaps', 'templates')
fs.readdir templatesDir, (err, files) =>
return unless files and files instanceof Array
templates = files.filter (filename) =>
path.extname(filename) is '.cson'
@setState(templates: templates)
_getStateFromKeymaps: =>
bindings = {}
for [command, label] in DisplayedKeybindings
[found] = atom.keymaps.findKeyBindings(command: command, target: document.body) || []
bindings[command] = found
bindings
render: =>
<div className="container-keymaps">
<Flexbox className="shortcut shortcut-select">
<div className="shortcut-name">Keyboard shortcut set:</div>
<div className="shortcut-value">
<select
style={margin:0}
value={@props.config.get('core.keymapTemplate')}
onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }>
{ @state.templates.map (template) =>
<option key={template} value={template}>{path.basename(template, '.cson')}</option>
}
</select>
</div>
</Flexbox>
{@_renderBindings()}
<div className="shortcuts-extras">
<button className="btn" onClick={@_onShowUserKeymaps}>Edit custom shortcuts</button>
</div>
</div>
_renderBindingFor: ([command, label]) =>
description = "None"
if @state.bindings[command]
{keystrokes} = @state.bindings[command]
description = keystrokes.replace(/-/gi,'').replace(/cmd/gi, '⌘').replace(/alt/gi, '⌥').replace(/shift/gi, '⇧').replace(/ctrl/gi, '^').toUpperCase()
<Flexbox className="shortcut" key={command}>
<div className="shortcut-name">{label}</div>
<div className="shortcut-value">{description}</div>
</Flexbox>
_renderBindings: =>
DisplayedKeybindings.map(@_renderBindingFor)
_onShowUserKeymaps: =>
require('shell').openItem(atom.keymaps.getUserKeymapPath())
module.exports = PreferencesKeymaps

View file

@ -0,0 +1,12 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesMailRules extends React.Component
@displayName: 'PreferencesMailRules'
render: =>
<div className="container-mail-rules">
</div>
module.exports = PreferencesMailRules

View file

@ -0,0 +1,56 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesNotifications extends React.Component
@displayName: 'PreferencesNotifications'
@propTypes:
config: React.PropTypes.object.isRequired
render: =>
<div className="container-notifications">
<div className="section">
<div className="section-header">
Notifications:
</div>
<div className="section-body">
<p className="platform-darwin-only">
<input type="checkbox"
id="core.showUnreadBadge"
checked={@props.config.get('core.showUnreadBadge')}
onChange={ => @props.config.toggle('core.showUnreadBadge')}/>
<label htmlFor="core.showUnreadBadge">Badge dock icon with unread message count</label>
</p>
<p>
<input type="checkbox"
id="unread-notifications.enabled"
checked={@props.config.get('unread-notifications.enabled')}
onChange={ => @props.config.toggle('unread-notifications.enabled')}/>
<label htmlFor="unread-notifications.enabled">Show notifications for new unread messages</label>
</p>
</div>
</div>
<div className="section-header">
Sounds:
</div>
<div className="section-body">
<p>
<input type="checkbox"
id="unread-notifications.sounds"
checked={@props.config.get('unread-notifications.sounds')}
onChange={ => @props.config.toggle('unread-notifications.sounds')}/>
<label htmlFor="unread-notifications.sounds">Play sound when receiving new mail</label>
</p>
<p>
<input type="checkbox"
id="core.sending.sounds"
checked={@props.config.get('core.sending.sounds')}
onChange={ => @props.config.toggle('core.sending.sounds')}/>
<label htmlFor="core.sending.sounds">Play sound when a message is sent</label>
</p>
</div>
</div>
module.exports = PreferencesNotifications

View file

@ -0,0 +1,53 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesSignatures extends React.Component
@displayName: 'PreferencesSignatures'
render: =>
<div className="container-signatures">
<div className="section-signaturces">
<div className="section-title">
Signatures
</div>
<div className="section-body">
<Flexbox direction="row" style={alignItems: "top"}>
<div style={flex: 2}>
<div className="menu">
<ul className="menu-items">
<li>Personal</li>
<li>Corporate</li>
</ul>
</div>
<div className="menu-footer">
<div className="menu-horizontal">
<ul className="menu-items">
<li>+</li>
<li>-</li>
</ul>
</div>
</div>
</div>
<div style={flex: 5}>
<div className="signature-area">
Signature
</div>
<div className="signature-footer">
<button className="edit-html-button btn">Edit HTML</button>
<div className="menu-horizontal">
<ul className="menu-items">
<li><b>B</b></li>
<li><i>I</i></li>
<li><u>u</u></li>
</ul>
</div>
</div>
</div>
</Flexbox>
</div>
</div>
</div>
module.exports = PreferencesSignatures

View file

@ -0,0 +1,16 @@
{
"name": "preferences",
"version": "0.1.0",
"main": "./lib/main",
"description": "Nylas Preferences Window Component",
"license": "Proprietary",
"private": true,
"engines": {
"atom": "*"
},
"dependencies": {
},
"windowTypes": {
"preferences": true
}
}

View file

@ -0,0 +1,263 @@
@import "ui-variables";
@import "ui-mixins";
body.is-blurred {
.preference-header {
background-image: -webkit-linear-gradient(top, lighten(@toolbar-background-color, 12%), lighten(@toolbar-background-color, 15%));
}
}
body.platform-darwin {
.preferences-wrap .platform-darwin-only {
display: inherit;
}
}
body.window-type-react-remote {
.sheet-toolbar {
border-bottom: none;
height:30px;
min-height:30px;
max-height:30px;
.toolbar-window-controls {
margin-top:3px;
margin-left:3px;
}
.window-title {
line-height:30px;
}
}
.sheet {
background: @background-secondary;
}
}
.preferences-wrap {
input[type=checkbox] {margin: 0px 7px 0px 0px; position: relative; top: -1px; }
input[type=radio] {margin: 0px 7px 0px 0px; position: relative; top: -1px; }
select { margin:10px; margin-top:4px; }
.platform-darwin-only {
display: none;
}
.well {
border-radius: 7px;
border:1px solid @border-color-divider;
background-color: lighten(@background-secondary, 2%);
&.large {
padding: 20px;
padding-right:10px;
}
&.small {
padding:10px;
}
}
.section {
margin-top:25px;
}
.section-header {
line-height: 2em;
margin-top:7px;
margin-bottom:2px;
cursor: default;
}
.section-body {
padding:5px;
padding-top: 10px;
padding-bottom: 10px;
.section-body-list {
flex: 1;
img { width:100%; padding:15px;}
}
}
.container-signatures {
width:95%;
margin-left: 2.5%;
margin-right: 2.5%;
.section-body {
padding: 10px 0px 0px 0px;
.menu {
border: solid thin #CCC;
margin-right: 5px;
min-height: 200px;
.menu-items {
margin:0;
padding:0;
list-style: none;
li { padding: 6px; }
}
}
.menu-horizontal {
height: 100%;
.menu-items {
height:100%;
margin: 0;
padding: 0;
list-style: none;
li {
text-align:center;
width:40px;
display:inline-block;
padding:8px 16px 8px 16px;
border-right: solid thin #CCC;
}
}
}
.signature-area {
border: solid thin #CCC;
min-height: 200px;
}
.menu-footer {
border: solid thin #CCC;
overflow: auto;
}
.signature-footer {
border: solid thin #CCC;
overflow: auto;
.edit-html-button {
float: right;
margin: 6px;
}
}
}
}
.container-notifications {
width: 75%;
margin-left: 12.5%;
margin-right: 12.5%;
}
.container-appearance {
width:70%;
margin: auto;
.appearance-mode {
background-color:#f7f9f9;
border-radius: 10px;
border: 1px solid #c6c7c7;
text-align: center;
flex: 1;
padding:25px;
padding-bottom:9px;
margin:10px;
margin-top:0;
img {
background-color: #c6c7c7;
}
div {
margin-top: 15px;
text-transform: capitalize;
cursor: default;
}
}
.appearance-mode.active {
border:1px solid @component-active-color;
color: @component-active-color;
img { background-color: @component-active-color; }
}
}
.container-keymaps {
width:75%;
margin-left:12.5%;
margin-right:12.5%;
.shortcut {
padding: 3px 0px;
&.shortcut-select {
padding: 5px 0px 20px 0px;
select {
font-size: @font-size-larger;
width: 75%;
}
}
.shortcut-name {
text-align: right;
flex: 1;
margin-right: 20px;
}
.shortcut-value {
text-align: left;
flex: 1;
}
}
.shortcuts-extras {
text-align:center;
margin-top: 20px;
}
}
.container-accounts {
margin-left:20px;
margin-right:20px;
.account-name {
font-size: @font-size-larger;
}
.account-subtext {
font-size: @font-size-small;
}
}
.preference-header {
margin-bottom:15px;
border-bottom: 1px solid darken(@toolbar-background-color, 9%);
background-image: -webkit-linear-gradient(top, @toolbar-background-color, darken(@toolbar-background-color, 2%));
cursor: default;
-webkit-user-select: none;
-webkit-app-region: drag;
.preference-header-item-spacer {
flex: 1;
}
.preference-header-item {
margin-left:3px;
margin-right:3px;
padding-left:3px;
padding-right:3px;
-webkit-app-region: no-drag;
display: block;
.phi-container {
padding-left:5%;
padding-right:5%;
padding-top: 3px;
text-align: center;
height:58px;
.icon {
height:33px;
}
.name {
font-size:12px;
line-height:22px;
white-space:nowrap;
}
}
&.active {
background-color:rgba(0,0,0, 0.12);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
}
}
}

View file

@ -21,7 +21,6 @@ class ThreadListStore extends NylasStore
@_resetInstanceVars()
@listenTo Actions.searchQueryCommitted, @_onSearchCommitted
@listenTo Actions.selectLayoutMode, @_autofocusForLayoutMode
@listenTo Actions.archiveAndPrevious, @_onArchiveAndPrev
@listenTo Actions.archiveAndNext, @_onArchiveAndNext
@ -39,6 +38,8 @@ class ThreadListStore extends NylasStore
@listenTo NamespaceStore, @_onNamespaceChanged
@listenTo FocusedCategoryStore, @_onCategoryChanged
atom.config.observe 'core.workspace.mode', => @_autofocusForLayoutMode()
# We can't create a @view on construction because the CategoryStore
# has hot yet been populated from the database with the list of
# categories and their corresponding ids. Once that is ready, the
@ -221,6 +222,7 @@ class ThreadListStore extends NylasStore
Actions.queueTask(task)
_autofocusForLayoutMode: ->
return unless @_view
layoutMode = WorkspaceStore.layoutMode()
focused = FocusedContentStore.focused('thread')
if layoutMode is 'split' and not focused and @_view.selection.count() is 0

View file

@ -6,6 +6,12 @@ _ = require 'underscore'
NamespaceStore} = require 'nylas-exports'
module.exports =
config:
enabled:
type: 'boolean'
default: true
activate: ->
@unlisteners = []
@unlisteners.push Actions.didPassivelyReceiveNewModels.listen(@_onNewMailReceived, @)
@ -51,6 +57,8 @@ module.exports =
_onNewMailReceived: (incoming) ->
new Promise (resolve, reject) =>
return resolve() if atom.config.get('unread-notifications.enabled') is false
incomingMessages = incoming['message'] ? []
incomingThreads = incoming['thread'] ? []

View file

@ -5,33 +5,12 @@
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
'body':
'c' : 'application:new-message' # Gmail
'/' : 'application:focus-search' # Gmail
'cmd-alt-f': 'application:focus-search'
'r' : 'application:reply' # Gmail
'R' : 'application:reply-all' # Nylas Mail
'a' : 'application:reply-all' # Gmail
'f' : 'application:forward' # Gmail
'l' : 'application:change-category' # Gmail
'escape': 'application:pop-sheet'
'u' : 'application:pop-sheet' # Gmail
'k' : 'core:previous-item' # Gmail
'up' : 'core:previous-item' # Mac mail
'j' : 'core:next-item' # Gmail
'down' : 'core:next-item' # Mac mail
']' : 'core:remove-and-previous' # Gmail
'[' : 'core:remove-and-next' # Gmail
'e' : 'core:remove-item' # Gmail
's' : 'core:star-item' #Gmail
'cmd-L' : 'core:star-item' # Mac mail
'ctrl-G' : 'core:star-item' # Outlook
'delete' : 'core:remove-item' # Mac mail
'backspace': 'core:remove-item' # Outlook
'escape' : 'application:pop-sheet'
'cmd-,' : 'application:open-preferences'
'up' : 'core:previous-item'
'down' : 'core:next-item'
'backspace': 'core:remove-item'
'enter' : 'core:focus-item'
'x' : 'core:select-item'
# Default cross-platform core behaviors
'left': 'core:move-left'
@ -213,5 +192,3 @@
# it.
'tab': 'native!'
'shift-tab': 'native!'
# Tokenizing Text fields

View file

@ -2,14 +2,6 @@
# to be listed in this file.
'body':
# Mac email-specific menu items
'cmd-n' : 'application:new-message' # Mac mail
'cmd-r' : 'application:reply' # Mac mail
'cmd-R' : 'application:reply-all' # Mac mail
'cmd-F' : 'application:forward' # Mac mail
'cmd-alt-f': 'application:focus-search' # Mac mail
'cmd-D': 'application:send-message' # Mac mail
# Mac application keys
'cmd-q': 'application:quit'
'cmd-h': 'application:hide'

View file

@ -3,12 +3,6 @@
# Linux email-specific menu items
'body':
'ctrl-n': 'application:new-message' # Outlook
'ctrl-r': 'application:reply' # Outlook
'ctrl-R': 'application:reply-all' # Outlook
'ctrl-F': 'application:forward' # Outlook
'ctrl-shift-v': 'application:change-category' # Outlook
# Linux application keys
'ctrl-q': 'application:quit'

View file

@ -0,0 +1,22 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
'body.platform-darwin':
'cmd-n' : 'application:new-message'
'cmd-r' : 'application:reply'
'cmd-R' : 'application:reply-all'
'cmd-F' : 'application:forward'
'cmd-alt-f': 'application:focus-search'
'cmd-D': 'application:send-message'
'cmd-V': 'application:change-category'
'delete' : 'core:remove-item'
'body.platform-linux, body.platform-win32':
'ctrl-n' : 'application:new-message'
'ctrl-r' : 'application:reply'
'ctrl-R' : 'application:reply-all'
'ctrl-F' : 'application:forward'
'ctrl-alt-f': 'application:focus-search'
'ctrl-D': 'application:send-message'
'ctrl-shift-v': 'application:change-category'
'delete' : 'core:remove-item'

View file

@ -0,0 +1,25 @@
# Email-specific core key-mappings
#
# There are additional mappings in <platform>.cson files that bind
# menu items. In the future, we should break these into files like:
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
'body':
'c' : 'application:new-message' # Gmail
'/' : 'application:focus-search' # Gmail
'r' : 'application:reply' # Gmail
'R' : 'application:reply-all' # Nylas Mail
'a' : 'application:reply-all' # Gmail
'f' : 'application:forward' # Gmail
'l' : 'application:change-category' # Gmail
'u' : 'application:pop-sheet' # Gmail
'delete' : 'application:pop-sheet'
'k' : 'core:previous-item' # Gmail
'j' : 'core:next-item' # Gmail
']' : 'core:remove-and-previous' # Gmail
'[' : 'core:remove-and-next' # Gmail
'e' : 'core:remove-item' # Gmail
's' : 'core:star-item' #Gmail
'x' : 'core:select-item'

View file

@ -0,0 +1,34 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
'body.platform-darwin':
# Windows email-specific menu items
'cmd-shift-v': 'application:change-category' # Outlook
'F3': 'application:focus-search'
'cmd-e': 'application:focus-search'
'cmd-f': 'application:forward'
'cmd-shift-v': 'application:change-category'
'cmd-d': 'core:remove-item'
'alt-backspace':'core:undo'
'alt-s': 'application:send-message'
'cmd-r': 'application:reply'
'cmd-shift-r': 'application:reply-all'
'cmd-n' : 'application:new-message'
'cmd-shift-m': 'application:new-message'
'cmd-enter': 'send'
'body.platform-linux, body.platform-win32':
# Windows email-specific menu items
'ctrl-shift-v': 'application:change-category' # Outlook
'F3': 'application:focus-search'
'ctrl-e': 'application:focus-search'
'ctrl-f': 'application:forward'
'ctrl-shift-v': 'application:change-category'
'ctrl-d': 'core:remove-item'
'alt-backspace':'core:undo'
'alt-s': 'application:send-message'
'ctrl-r': 'application:reply'
'ctrl-shift-r': 'application:reply-all'
'ctrl-n' : 'application:new-message'
'ctrl-shift-m': 'application:new-message'
'ctrl-enter': 'send'

View file

@ -3,6 +3,9 @@
label: 'Nylas'
submenu: [
{ label: 'About Nylas', command: 'application:about' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ type: 'separator' }
{ label: 'Link External Account', command: 'atom-workspace:add-account' }
{ label: 'Log Out', command: 'application:logout' }
{ label: 'VERSION', enabled: false }

View file

@ -21,7 +21,7 @@ fs =
unlink: (path, callback) ->
callback(null) if callback
LaunchServices = proxyquire "../lib/launch-services",
LaunchServices = proxyquire "../src/launch-services",
"child_process": ChildProcess
"fs": fs

View file

@ -44,6 +44,9 @@ describe "MessageStore", ->
FocusedContentStore.trigger({impactsCollection: -> true})
testThread.unread = true
spyOn(Actions, 'queueTask')
spyOn(atom.config, 'get').andCallFake (key) =>
if key is 'core.reading.markAsReadDelay'
return 600
it "should queue a task to mark the thread as read", ->
@focus = testThread

View file

@ -160,7 +160,6 @@ class Atom extends Model
@setupErrorHandling()
@unsubscribe()
@setBodyPlatformClass()
@loadTime = null
@ -175,7 +174,10 @@ class Atom extends Model
MenuManager = require './menu-manager'
configDirPath = @getConfigDirPath()
{devMode, safeMode, resourcePath} = @getLoadSettings()
{devMode, safeMode, resourcePath, windowType} = @getLoadSettings()
document.body.classList.add("platform-#{process.platform}")
document.body.classList.add("window-type-#{windowType}")
# Add 'exports' to module search path.
exportsPath = path.join(resourcePath, 'exports')
@ -643,17 +645,15 @@ class Atom extends Model
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@keymaps.loadUserKeymap()
@packages.loadPackages(windowType)
@packages.loadPackage(pack) for pack in (windowPackages ? [])
@deserializeSheetContainer()
@packages.activate()
@keymaps.loadUserKeymap()
ipc.on("load-settings-changed", @loadSettingsChanged)
@keymaps.loadUserKeymap()
@setWindowDimensions({width, height}) if width and height
@menu.update()
@ -866,9 +866,6 @@ class Atom extends Model
updateAvailable: (details) ->
@emitter.emit 'update-available', details
setBodyPlatformClass: ->
document.body.classList.add("platform-#{process.platform}")
setAutoHideMenuBar: (autoHide) ->
ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide)
ipc.send('call-window-method', 'setMenuBarVisibility', !autoHide)

View file

@ -131,10 +131,10 @@ class Application
@setDatabasePhase('close')
@windowManager.closeMainWindow()
@windowManager.unregisterAllHotWindows()
@config.set('nylas', null)
@config.set('edgehill', null)
fs.unlink(path.join(configDirPath,'edgehill.db'))
@setDatabasePhase('setup')
fs.unlink path.join(configDirPath,'edgehill.db'), =>
@config.set('nylas', null)
@config.set('edgehill', null)
@setDatabasePhase('setup')
databasePhase: ->
@_databasePhase
@ -202,6 +202,7 @@ class Application
atomWindow?.browserWindow.inspectElement(x, y)
@on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
@on 'application:show-main-window', => @windowManager.ensurePrimaryWindowOnscreen()
@on 'application:check-for-update', => @autoUpdateManager.check()
@on 'application:install-update', =>
@ -270,6 +271,9 @@ class Application
ipc.on 'unregister-hot-window', (event, windowType) =>
@windowManager.unregisterHotWindow(windowType)
ipc.on 'from-react-remote-window', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window', json)
app.on 'activate-with-no-open-windows', (event) =>
@windowManager.ensurePrimaryWindowOnscreen()
event.preventDefault()

View file

@ -104,11 +104,9 @@ class AtomWindow
if @browserWindow.loadSettingsChangedSinceGetURL
@browserWindow.webContents.send('load-settings-changed', @browserWindow.loadSettings)
@browserWindow.loadUrl @getUrl(loadSettings)
@browserWindow.loadUrl(@getUrl(loadSettings))
@browserWindow.focusOnWebView() if @isSpec
@openPath(pathToOpen) unless @isSpecWindow()
loadSettings: -> @browserWindow.loadSettings
setLoadSettings: (loadSettings) ->
@ -199,9 +197,6 @@ class AtomWindow
@browserWindow.on 'blur', =>
@browserWindow.focusOnWebView() unless @isWindowClosing
openPath: (pathToOpen) ->
@sendMessage 'open-path', pathToOpen
sendMessage: (message, detail) ->
if @loaded
@browserWindow.webContents.send(message, detail)

View file

@ -125,6 +125,9 @@ class ComponentRegistry
{locations, modes, roles} = @_pluralizeDescriptor(descriptor)
if not locations and not modes and not roles
throw new Error("ComponentRegistry.findComponentsMatching called with an empty descriptor")
# Made into a convenience function because default
# values (`[]`) are necessary and it was getting messy.
overlaps = (entry = [], search = []) ->

View file

@ -7,6 +7,9 @@ module.exports =
core:
type: 'object'
properties:
showUnreadBadge:
type: 'boolean'
default: true
disabledPackages:
type: 'array'
default: []
@ -17,8 +20,32 @@ module.exports =
default: ['ui-light']
items:
type: 'string'
keymapTemplate:
type: 'string'
default: 'Gmail.cson'
attachments:
type: 'object'
properties:
downloadPolicy:
type: 'string'
default: 'on-read'
enum: ['on-receive', 'on-read', 'manually']
reading:
type: 'object'
properties:
markAsReadDelay:
type: 'integer'
default: 500
sending:
type: 'object'
properties:
sounds:
type: 'boolean'
default: true
defaultReplyType:
type: 'string'
default: 'reply-all'
enum: ['reply', 'reply-all']
if process.platform in ['win32', 'linux']
module.exports.core.properties.autoHideMenuBar =

View file

@ -136,6 +136,13 @@ class Actions
###
@showDeveloperConsole: ActionScopeWindow
###
Public: Register a preferences tab, usually applied in Preferences window
*Scope: Window*
###
@registerPreferencesTab: ActionScopeWindow
###
Public: Clear the developer console for the current window.
@ -164,17 +171,6 @@ class Actions
###
@selectRootSheet: ActionScopeWindow
###
Public: Select the desired layout mode.
*Scope: Window*
```
Actions.selectLayoutMode('list')
```
###
@selectLayoutMode: ActionScopeWindow
###
Public: Toggle whether a particular column is visible. Call this action
with one of the Sheet location constants:
@ -193,7 +189,7 @@ class Actions
*Scope: Window*
```
Actions.selectLayoutMode(collection: 'thread', item: <Thread>)
Actions.setCursorPosition(collection: 'thread', item: <Thread>)
```
###
@setCursorPosition: ActionScopeWindow

View file

@ -193,6 +193,9 @@ class Message extends Model
file.namespaceId = @namespaceId
return @
canReplyAll: ->
@cc.length > 0 or @to.length > 1
# Public: Returns a set of uniqued message participants by combining the
# `to`, `cc`, and `from` fields.
participants: ->

View file

@ -57,4 +57,13 @@ class Namespace extends Model
usesLabels: -> @organizationUnit is "label"
usesFolders: -> @organizationUnit is "folder"
# Public: Returns the localized, properly capitalized provider name,
# like Gmail, Exchange, or Outlook 365
displayProvider: ->
if @provider is 'eas'
return 'Exchange'
if @provider is 'gmail'
return 'Gmail'
return @provider
module.exports = Namespace

View file

@ -100,7 +100,7 @@ class NylasAPI
@APIRoot = 'https://api-staging.nylas.com'
else if env in ['experimental']
@AppID = 'c5dis00do2vki9ib6hngrjs18'
@APIRoot = 'https://api-experimental.nylas.com'
@APIRoot = 'https://api-staging-experimental.nylas.com'
else if env in ['local']
@AppID = 'n/a'
@APIRoot = 'http://localhost:5555'

View file

@ -113,6 +113,7 @@ FileDownloadStore = Reflux.createStore
@listenTo Actions.fetchAndOpenFile, @_fetchAndOpen
@listenTo Actions.fetchAndSaveFile, @_fetchAndSave
@listenTo Actions.abortFetchFile, @_abortFetchFile
@listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived
@_downloads = {}
@_downloadDirectory = "#{atom.getConfigDirPath()}/downloads"
@ -142,6 +143,12 @@ FileDownloadStore = Reflux.createStore
########### PRIVATE ####################################################
_newMailReceived: (incoming) =>
return unless atom.config.get('core.attachments.downloadPolicy') is 'on-receive'
for message in incoming
for file in message.files
@_fetch(file)
# Returns a promise with a Download object, allowing other actions to be
# daisy-chained to the end of the download operation.
_startDownload: (file) ->

View file

@ -173,19 +173,19 @@ class MessageStore extends NylasStore
# and once when ready. Many third-party stores will observe
# MessageStore and they'll be stupid and re-render constantly.
if loaded
# Mark the thread as read if necessary. Wait 700msec so that flipping
# through threads doens't mark them all. Make sure it's still the
# Mark the thread as read if necessary. Make sure it's still the
# current thread after the timeout.
# Override canBeUndone to return false so that we don't see undo prompts
# (since this is a passive action vs. a user-triggered action.)
if @_thread.unread
markAsReadDelay = atom.config.get('core.reading.markAsReadDelay')
setTimeout =>
return unless loadedThreadId is @_thread?.id
t = new ChangeUnreadTask(thread: @_thread, unread: false)
t.canBeUndone = => false
Actions.queueTask(t)
,700
, markAsReadDelay
@_itemsLoading = false
@trigger(@)
@ -200,6 +200,7 @@ class MessageStore extends NylasStore
startedAFetch
_fetchExpandedAttachments: (items) ->
return unless atom.config.get('core.attachments.downloadPolicy') is 'on-read'
for item in items
continue unless @_itemsExpanded[item.id]
for file in item.files

View file

@ -21,6 +21,12 @@ UnreadCountStore = Reflux.createStore
@listenTo NamespaceStore, @_onNamespaceChanged
@listenTo DatabaseStore, @_onDataChanged
atom.config.observe 'core.showUnreadBadge', (val) =>
if val is true
@_updateBadgeForCount()
else
@_setBadge("")
@_count = null
_.defer => @_fetchCount()
@ -70,6 +76,7 @@ UnreadCountStore = Reflux.createStore
_updateBadgeForCount: (count) ->
return unless atom.isMainWindow()
return if atom.config.get('core.showUnreadBadge') is false
if count > 999
@_setBadge("999+")
else if count > 0

View file

@ -27,7 +27,6 @@ class WorkspaceStore
@_resetInstanceVars()
@listenTo Actions.selectRootSheet, @_onSelectRootSheet
@listenTo Actions.selectLayoutMode, @_onSelectLayoutMode
@listenTo Actions.setFocus, @_onSetFocus
@listenTo Actions.toggleWorkspaceLocationHidden, @_onToggleLocationHidden
@ -35,6 +34,12 @@ class WorkspaceStore
@listenTo Actions.popSheet, @popSheet
@listenTo Actions.searchQueryCommitted, @popToRootSheet
@_preferredLayoutMode = atom.config.get('core.workspace.mode')
atom.config.observe 'core.workspace.mode', (mode) =>
return if mode is @_preferredLayoutMode
@_preferredLayoutMode = mode
@trigger()
atom.commands.add 'body',
'application:pop-sheet': => @popSheet()
@ -42,7 +47,6 @@ class WorkspaceStore
@Location = Location = {}
@Sheet = Sheet = {}
@_preferredLayoutMode = 'list'
@_hiddenLocations = {}
@_sheetStack = []
@ -70,10 +74,6 @@ class WorkspaceStore
@_sheetStack.push(sheet)
@trigger(@)
_onSelectLayoutMode: (mode) =>
@_preferredLayoutMode = mode
@trigger(@)
_onToggleLocationHidden: (location) =>
if not location.id
throw new Error("Actions.toggleWorkspaceLocationHidden - pass a WorkspaceStore.Location")
@ -114,6 +114,9 @@ class WorkspaceStore
else
root.supportedModes[0]
preferredLayoutMode: =>
@_preferredLayoutMode
# Public: Returns The top {Sheet} in the current stack. Use this method to determine
# the sheet the user is looking at.
#

View file

@ -5,19 +5,29 @@ CSON = require 'season'
{jQuery} = require 'space-pen'
Grim = require 'grim'
bundledKeymaps = require('../package.json')?._atomKeymaps
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
@emitter.on 'did-load-bundled-keymaps', callback
KeymapManager::loadBundledKeymaps = ->
keymapsPath = path.join(@resourcePath, 'keymaps')
if bundledKeymaps?
for keymapName, keymap of bundledKeymaps
keymapPath = path.join(keymapsPath, keymapName)
@add(keymapPath, keymap)
else
@loadKeymap(keymapsPath)
# Load the base keymap and the base.platform keymap
baseKeymap = path.join(@resourcePath, 'keymaps', 'base.cson')
basePlatformKeymap = path.join(@resourcePath, 'keymaps', "base.#{process.platform}.cson")
@loadKeymap(baseKeymap)
@loadKeymap(basePlatformKeymap)
# Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
templateConfigKey = 'core.keymapTemplate'
templateKeymapPath = null
reloadTemplateKeymap = =>
@removeBindingsFromSource(templateKeymapPath) if templateKeymapPath
templateFile = atom.config.get(templateConfigKey)
if templateFile
templateKeymapPath = path.join(@resourcePath, 'keymaps', 'templates', templateFile)
@loadKeymap(templateKeymapPath)
@emitter.emit('did-reload-keymap', {path: templateKeymapPath})
atom.config.observe(templateConfigKey, reloadTemplateKeymap)
reloadTemplateKeymap()
@emit 'bundled-keymaps-loaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-load-bundled-keymaps'

View file

@ -66,6 +66,14 @@ class LaunchServices
return callback(def.LSHandlerRoleAll is bundleIdentifier)
callback(false)
resetURLScheme: (scheme, callback) ->
@readDefaults (defaults) =>
# Remove anything already registered for the scheme
for ii in [defaults.length-1..0] by -1
if defaults[ii].LSHandlerURLScheme is scheme
defaults.splice(ii, 1)
@writeDefaults(defaults, callback)
registerForURLScheme: (scheme, callback) ->
@readDefaults (defaults) =>
# Remove anything already registered for the scheme

100
src/react-remote/react-remote-child.js vendored Normal file
View file

@ -0,0 +1,100 @@
var container = document.getElementById("container");
var ipc = require('ipc');
document.body.classList.add("platform-"+process.platform);
document.body.classList.add("window-type-react-remote");
var receiveEvent = function (json) {
var remote = require('remote');
if (json.html) {
var browserWindow = remote.getCurrentWindow();
browserWindow.on('focus', function() {
document.body.classList.remove('is-blurred')
});
browserWindow.on('blur', function() {
document.body.classList.add('is-blurred')
});
container.innerHTML = json.html;
var style = document.createElement('style');
style.onload = function() {
for (var ii = 0; ii < json.waiting.length; ii ++) {
receiveEvent(json.waiting[ii]);
}
window.requestAnimationFrame(function() {
browserWindow.show();
});
};
style.textContent = json.style;
document.body.appendChild(style);
}
if (json.sel) {
var React = require('react');
var ReactMount = require('react/lib/ReactMount');
ReactMount.getNode = function(id) {
return document.querySelector("[data-reactid='"+id+"']");
};
var sources = {
CSSPropertyOperations: require('react/lib/CSSPropertyOperations'),
DOMPropertyOperations: require('react/lib/DOMPropertyOperations'),
DOMChildrenOperations: require('react/lib/DOMChildrenOperations'),
ReactDOMIDOperations: require('react/lib/ReactDOMIDOperations'),
Custom: {
setSelectCurrentValue: function(reactid, value) {
var children = ReactMount.getNode(reactid).childNodes;
for (var ii = 0; ii < children.length; ii ++) {
children[ii].selected = (children[ii].value == value);
}
}
}
};
if (json.firstArgType == 'node') {
json.arguments[0] = ReactMount.getNode(json.arguments[0]);
} else if (json.firstArgType == 'array') {
for (var ii = 0; ii < json.arguments[0].length; ii ++) {
json.arguments[0][ii].parentNode = ReactMount.getNode(json.arguments[0][ii].parentNode)
}
}
sources[json.parent][json.sel].apply(sources[json.parent], json.arguments);
}
};
ipc.on("to-react-remote-window", receiveEvent);
var events = ['keypress', 'keydown', 'keyup', 'change', 'submit', 'click', 'focus', 'blur', 'input', 'select'];
events.forEach(function(type) {
container.addEventListener(type, function(event) {
var representation = {
eventType: event.type,
eventClass: event.constructor.name,
pageX: event.pageX,
pageY: event.pageY,
bubbles: event.bubbles,
cancelable: event.cancelable,
clientX: event.clientX,
clientY: event.clientY,
charCode: event.charCode,
keyCode: event.keyCode,
detail: event.detail,
eventPhase: event.eventPhase
}
if (event.target) {
representation.targetReactId = event.target.dataset.reactid;
}
if (event.target.value !== undefined) {
representation.targetValue = event.target.value;
}
if (event.target.checked !== undefined) {
representation.targetChecked = event.target.checked;
}
var remote = require('remote');
ipc.send("from-react-remote-window", {windowId: remote.getCurrentWindow().id, event: representation});
event.preventDefault();
}, true);
});

310
src/react-remote/react-remote-parent.js vendored Normal file
View file

@ -0,0 +1,310 @@
var ipc = require("ipc");
var React = require('react');
var LinkedValueUtils = require('react/lib/LinkedValueUtils');
var ReactDOMComponent = require('react/lib/ReactDOMComponent');
var methods = Object.keys(ReactDOMComponent.BackendIDOperations);
var invocationTargets = [];
var sources = {
CSSPropertyOperations: require('react/lib/CSSPropertyOperations'),
DOMPropertyOperations: require('react/lib/DOMPropertyOperations'),
DOMChildrenOperations: require('react/lib/DOMChildrenOperations'),
ReactDOMIDOperations: require('react/lib/ReactDOMIDOperations'),
ReactDOMSelect: require('react/lib/ReactDOMSelect')
}
var Custom = {
sendSelectCurrentValue: function() {
var reactid = this.getDOMNode().dataset.reactid;
var target = invocationTargetForReactId(reactid);
if (target) {
var value = LinkedValueUtils.getValue(this);
target.send({
parent: 'Custom',
sel: 'setSelectCurrentValue',
arguments: [reactid, LinkedValueUtils.getValue(this)]
});
}
}
};
var invocationTargetForReactId = function(id) {
for (var ii = 0; ii < invocationTargets.length; ii++) {
var target = invocationTargets[ii];
if (id.substr(0, target.reactid.length) == target.reactid) {
return target;
}
if (target.reactid == 'not-yet-rendered') {
var node = document.querySelector("[data-reactid='"+id+"']");
while (node = node.parentNode) {
if (node == target.container) {
return target;
}
}
}
}
return null;
};
var observeMethod = function(parent, sel, callback) {
var owner = sources[parent];
if (!owner[sel]) {
owner = owner.prototype;
}
var oldImpl = owner[sel];
owner[sel] = function() {
oldImpl.apply(this, arguments);
if (invocationTargets.length == 0)
return;
callback.apply(this, arguments);
}
};
var observeMethodAndBroadcast = function(parent, sel) {
observeMethod(parent, sel, function() {
var id = null;
var target = null;
var firstArgType = null;
var args = [];
for (var ii = 0; ii < arguments.length; ii ++) {
args.push(arguments[ii]);
}
if (arguments[0] instanceof Node) {
args[0] = args[0].dataset.reactid;
target = invocationTargetForReactId(args[0]);
firstArgType = "node";
} else if (typeof(args[0]) === 'string') {
target = invocationTargetForReactId(args[0]);
firstArgType = "id";
} else if (args[0] instanceof Array) {
for (var ii = 0; ii < args[0].length; ii ++) {
args[0][ii].parentNode = args[0][ii].parentNode.dataset.reactid;
}
target = invocationTargetForReactId(args[0][0].parentNode);
firstArgType = "array";
}
if (target) {
target.send({
parent: parent,
sel: sel,
arguments: args,
firstArgType: firstArgType
});
}
});
};
setTimeout(function(){
observeMethodAndBroadcast('CSSPropertyOperations', 'setValueForStyles');
observeMethodAndBroadcast('DOMChildrenOperations', 'updateTextContent');
observeMethodAndBroadcast('DOMChildrenOperations', 'dangerouslyReplaceNodeWithMarkup');
observeMethodAndBroadcast('DOMPropertyOperations', 'deleteValueForProperty');
observeMethodAndBroadcast('DOMPropertyOperations', 'setValueForProperty');
observeMethodAndBroadcast('ReactDOMIDOperations', 'updateInnerHTMLByID');
observeMethodAndBroadcast('DOMChildrenOperations', 'processUpdates');
observeMethod('ReactDOMSelect', 'componentDidUpdate', Custom.sendSelectCurrentValue);
observeMethod('ReactDOMSelect', 'componentDidMount', Custom.sendSelectCurrentValue);
}, 10);
ipc.on('from-react-remote-window', function(json) {
var container = null;
for (var ii = 0; ii < invocationTargets.length; ii ++) {
if (invocationTargets[ii].windowId == json.windowId) {
container = invocationTargets[ii].container;
}
}
if (!container) {
console.error("Received message from child window "+json.windowId+" which is not recognized.");
return;
}
if (json.event) {
var rep = json.event;
if (rep.targetReactId) {
rep.target = document.querySelector(["[data-reactid='"+rep.targetReactId+"']"]);
}
if (rep.target && (rep.targetValue !== undefined)) {
rep.target.value = rep.targetValue;
}
if (rep.target && (rep.targetChecked !== undefined)) {
rep.target.checked = rep.targetChecked;
}
var EventClass = {
"MouseEvent": MouseEvent,
"KeyboardEvent": KeyboardEvent,
"FocusEvent": FocusEvent
}[rep.eventClass] || Event;
var e = new EventClass(rep.eventType, rep);
process.nextTick(function() {
if (rep.target) {
rep.target.dispatchEvent(e);
} else {
container.dispatchEvent(e);
}
});
}
});
var parentListenersAttached = false;
var reactRemoteContainer = document.createElement('div');
reactRemoteContainer.style.display = 'none';
reactRemoteContainer.style.backgroundColor = 'white';
reactRemoteContainer.style.position = 'absolute';
reactRemoteContainer.style.zIndex = 10000;
reactRemoteContainer.style.border = '5px solid orange';
document.body.appendChild(reactRemoteContainer);
var toggleContainerVisible = function() {
if (reactRemoteContainer.style.display === 'none') {
reactRemoteContainer.style.display = 'inherit';
} else {
reactRemoteContainer.style.display = 'none';
}
};
var openWindowForComponent = function(Component, options) {
// If a tag is specified, see if we can find an existing window to bring to foreground
if (options.tag) {
for (var ii = 0; ii < invocationTargets.length; ii++) {
if (invocationTargets[ii].tag === options.tag) {
invocationTargets[ii].window.focus();
return;
}
}
}
var remote = require('remote');
var url = require('url');
var BrowserWindow = remote.require('browser-window');
// Read rendered styles out of the page
var styles = document.querySelectorAll("style");
var thinStyles = "";
for (var ii = 0; ii < styles.length; ii++) {
var styleNode = styles[ii];
if (!styleNode.sourcePath) {
continue;
}
if ((styleNode.sourcePath.indexOf("static/index") > 0) || (options.stylesheetRegex && options.stylesheetRegex.test(styleNode.sourcePath))) {
thinStyles = thinStyles + styleNode.innerText;
}
}
// Create a browser window
var thinWindowUrl = url.format({
protocol: 'file',
pathname: atom.getLoadSettings().resourcePath+"/static/react-remote-child.html",
slashes: true
});
var thinWindow = new BrowserWindow({
frame: false,
width: options.width || 800,
height: options.height || 600,
resizable: options.resizable,
show: false
});
thinWindow.loadUrl(thinWindowUrl);
// Add a container to our local document to hold the root component of the window
var container = document.createElement('div');
container.id = 'react-remote-window-container-'+thinWindow.id;
if (options.width) {
container.style.width = options.width+'px';
}
if (options.height) {
container.style.height = options.height+'px';
}
reactRemoteContainer.appendChild(container);
var cleanup = function() {
if (container == null) {
return;
}
for (var ii = 0; ii < invocationTargets.length; ii++) {
if (invocationTargets[ii].container === container) {
invocationTargets.splice(ii, 1);
break;
}
}
reactRemoteContainer.removeChild(container);
console.log("Cleaned up react remote window");
container = null;
thinWindow = null;
};
var sendWaiting = [];
// Create a "Target" object that we'll use to store information about the
// remote window, it's reactId, etc.
var target = {
container: container,
containerReady: false,
window: thinWindow,
windowReady: false,
windowId: thinWindow.id,
tag: options.tag,
reactid: 'not-yet-rendered',
send: function(args) {
if (target.containerReady && target.windowReady) {
thinWindow.webContents.send('to-react-remote-window', args);
} else {
sendWaiting.push(args);
}
},
sendHTMLIfReady: function() {
if (target.containerReady && target.windowReady) {
thinWindow.webContents.send('to-react-remote-window', {
html: container.innerHTML,
style: thinStyles,
waiting: sendWaiting
});
}
}
};
invocationTargets.push(target);
// Finally, render the react component into our local container and open
// the browser window. When both of these things finish, we send the html
// css, and any observed method invocations that occurred during the first
// React cycle (componentDidMount).
React.render(React.createElement(Component), container, function() {
target.reactid = container.firstChild.dataset.reactid,
target.containerReady = true;
target.sendHTMLIfReady();
});
thinWindow.on('closed', cleanup);
thinWindow.webContents.on('crashed', cleanup);
thinWindow.webContents.on('did-finish-load', function () {
target.windowReady = true;
target.sendHTMLIfReady();
});
// The first time a remote window is opened, add event listeners to our
// own window so that we close dependent windows when we're closed.
if (parentListenersAttached == false) {
remote.getCurrentWindow().on('close', function() {
for (var ii = 0; ii < invocationTargets.length; ii++) {
invocationTargets[ii].window.close();
}
invocationTargets = [];
})
parentListenersAttached = true;
}
};
module.exports = {
openWindowForComponent: openWindowForComponent,
toggleContainerVisible: toggleContainerVisible
};

View file

@ -0,0 +1,37 @@
path = require('path')
fs = require('fs-plus')
ipc = require('ipc')
require('module').globalPaths.push(path.resolve('exports'))
# Swap out Node's native Promise for Bluebird, which allows us to
# do fancy things like handle exceptions inside promise blocks
global.Promise = require 'bluebird'
global.atom =
commands:
add: ->
remove: ->
config:
get: -> null
set: ->
onDidChange: ->
onBeforeUnload: ->
getWindowLoadTime: -> 0
getConfigDirPath: ->
@configDirPath ?= fs.absolute('~/.nylas')
getLoadSettings: ->
@loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
inSpecMode: ->
false
isMainWindow: ->
false
# Like sands through the hourglass, so are the days of our lives.
require './window'
prefs = require '../internal_packages/preferences/lib/main'
prefs.activate()
ipc.on 'command', (command, args) ->
if command is 'window:toggle-dev-tools'
ipc.send('call-window-method', 'toggleDevTools')

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -0,0 +1,19 @@
<body>
<div class="sheet-container">
<div class="sheet-toolbar">
<div style="position:absolute; width:100%; height:100%; z-index: 1;" class="sheet-toolbar-container">
<div name="ToolbarWindowControls" class="toolbar-window-controls">
<button class="close" onClick="require('remote').getCurrentWindow().close()"></button>
<button class="minimize" onClick="require('remote').getCurrentWindow().minimize()"></button>
<button class="maximize" onClick="require('remote').getCurrentWindow().maximize()"></button>
</div>
<div class="window-title"></div>
</div>
</div>
<div id="container" style="left:0; right:0; position:absolute;"></div>
</div>
<script>
require('../src/react-remote/react-remote-child')
</script>
</body>