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
6
.gitignore
vendored
|
@ -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
|
||||
*~
|
||||
*#
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}/>
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
57
internal_packages/preferences/lib/main.cjsx
Normal 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
|
34
internal_packages/preferences/lib/preferences-header.cjsx
Normal 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
|
19
internal_packages/preferences/lib/preferences-store.coffee
Normal 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()
|
73
internal_packages/preferences/lib/preferences.cjsx
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
16
internal_packages/preferences/package.json
Normal 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
|
||||
}
|
||||
}
|
263
internal_packages/preferences/stylesheets/preferences.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'] ? []
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
|
@ -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'
|
||||
|
22
keymaps/templates/Apple Mail.cson
Normal 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'
|
25
keymaps/templates/Gmail.cson
Normal 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'
|
34
keymaps/templates/Outlook.cson
Normal 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'
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = []) ->
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
||||
};
|
37
src/window-thin-bootstrap.coffee
Normal 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')
|
BIN
static/images/preferences/appearance/appearance-mode-list@2x.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 7 KiB |
After Width: | Height: | Size: 9.2 KiB |
BIN
static/images/preferences/tabs/ic-settings-accounts@2x.png
Executable file
After Width: | Height: | Size: 6.5 KiB |
BIN
static/images/preferences/tabs/ic-settings-appearance@2x.png
Executable file
After Width: | Height: | Size: 8.7 KiB |
BIN
static/images/preferences/tabs/ic-settings-general@2x.png
Executable file
After Width: | Height: | Size: 10 KiB |
BIN
static/images/preferences/tabs/ic-settings-mailrules@2x.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
BIN
static/images/preferences/tabs/ic-settings-notifications@2x.png
Executable file
After Width: | Height: | Size: 8.3 KiB |
BIN
static/images/preferences/tabs/ic-settings-shortcuts@2x.png
Executable file
After Width: | Height: | Size: 5.8 KiB |
BIN
static/images/preferences/tabs/ic-settings-signatures@2x.png
Executable file
After Width: | Height: | Size: 8.3 KiB |
19
static/react-remote-child.html
Normal 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>
|