diff --git a/internal_packages/account-error-header/assets/icon-alert-onred@1x.png b/internal_packages/account-error-header/assets/icon-alert-onred@1x.png new file mode 100644 index 000000000..734d5d7fd Binary files /dev/null and b/internal_packages/account-error-header/assets/icon-alert-onred@1x.png differ diff --git a/internal_packages/account-error-header/assets/icon-alert-onred@2x.png b/internal_packages/account-error-header/assets/icon-alert-onred@2x.png new file mode 100644 index 000000000..f76b1a1bd Binary files /dev/null and b/internal_packages/account-error-header/assets/icon-alert-onred@2x.png differ diff --git a/internal_packages/account-error-header/assets/icon-alert-sourcelist@1x.png b/internal_packages/account-error-header/assets/icon-alert-sourcelist@1x.png new file mode 100644 index 000000000..a2e488fbc Binary files /dev/null and b/internal_packages/account-error-header/assets/icon-alert-sourcelist@1x.png differ diff --git a/internal_packages/account-error-header/assets/icon-alert-sourcelist@2x.png b/internal_packages/account-error-header/assets/icon-alert-sourcelist@2x.png new file mode 100644 index 000000000..048627605 Binary files /dev/null and b/internal_packages/account-error-header/assets/icon-alert-sourcelist@2x.png differ diff --git a/internal_packages/account-error-header/lib/account-error-header.jsx b/internal_packages/account-error-header/lib/account-error-header.jsx new file mode 100644 index 000000000..f731a7b26 --- /dev/null +++ b/internal_packages/account-error-header/lib/account-error-header.jsx @@ -0,0 +1,93 @@ +import {AccountStore, Account, Actions, React} from 'nylas-exports' +import {RetinaImg} from 'nylas-component-kit' + +export default class AccountErrorHeader extends React.Component { + static displayName = 'AccountErrorHeader'; + + constructor() { + super(); + this.state = this.getStateFromStores(); + } + + componentDidMount() { + this.unsubscribe = AccountStore.listen(() => this._onAccountsChanged()); + } + + getStateFromStores() { + return {accounts: AccountStore.accounts()} + } + + _onAccountsChanged() { + this.setState(this.getStateFromStores()) + } + + _reconnect(account) { + const ipc = require('electron').ipcRenderer; + ipc.send('command', 'application:add-account', account.provider); + } + + _openPreferences() { + Actions.switchPreferencesTab('Accounts'); + Actions.openPreferences() + } + + _contactSupport() { + const {shell} = require("electron"); + shell.openExternal("https://support.nylas.com/hc/en-us/requests/new"); + } + + renderErrorHeader(message, buttonName, actionCallback) { + return ( +
+
+
+
+ +
{message}
+ + {buttonName} + +
+
) + } + + render() { + const errorAccounts = this.state.accounts.filter(a => a.syncState !== "running"); + if (errorAccounts.length === 1) { + const account = errorAccounts[0]; + + switch (account.syncState) { + + case Account.SYNC_STATE_AUTH_FAILED: + return this.renderErrorHeader( + `Nylas N1 can no longer authenticate with ${account.emailAddress}. Click here to reconnect.`, + "Reconnect", + ()=>this._reconnect(account)); + + case Account.SYNC_STATE_STOPPED: + return this.renderErrorHeader( + `The cloud sync for ${account.emailAddress} has been disabled. You will + not be able to send or receive mail. Please contact Nylas support.`, + "Contact support", + ()=>this._contactSupport()); + + default: + return this.renderErrorHeader( + `Nylas encountered an error while syncing mail for ${account.emailAddress} - we're + looking into it. Contact Nylas support for details.`, + "Contact support", + ()=>this._contactSupport()); + } + } + if (errorAccounts.length > 1) { + return this.renderErrorHeader("Several of your accounts are having issues. " + + "You will not be able to send or receive mail. Click here to manage your accounts.", + "Open preferences", + ()=>this._openPreferences()); + } + return false; + } +} diff --git a/internal_packages/account-error-header/lib/main.es6 b/internal_packages/account-error-header/lib/main.es6 new file mode 100644 index 000000000..51f23443b --- /dev/null +++ b/internal_packages/account-error-header/lib/main.es6 @@ -0,0 +1,12 @@ +import {ComponentRegistry, WorkspaceStore} from 'nylas-exports'; +import AccountErrorHeader from './account-error-header'; + +export function activate() { + ComponentRegistry.register(AccountErrorHeader, {location: WorkspaceStore.Sheet.Threads.Header}); +} + +export function serialize() {} + +export function deactivate() { + ComponentRegistry.unregister(AccountErrorHeader); +} diff --git a/internal_packages/account-error-header/package.json b/internal_packages/account-error-header/package.json new file mode 100755 index 000000000..6562099c6 --- /dev/null +++ b/internal_packages/account-error-header/package.json @@ -0,0 +1,13 @@ +{ + "name": "account-error-header", + "version": "0.1.0", + "main": "./lib/main", + "description": "Header to display errors syncing the active account", + "license": "GPL-3.0", + "private": true, + "engines": { + "nylas": "*" + }, + "dependencies": { + } +} diff --git a/internal_packages/account-error-header/stylesheets/account-error-header.less b/internal_packages/account-error-header/stylesheets/account-error-header.less new file mode 100644 index 000000000..47a45a88a --- /dev/null +++ b/internal_packages/account-error-header/stylesheets/account-error-header.less @@ -0,0 +1,22 @@ +@import "ui-variables"; +@import "ui-mixins"; + + +.sync-issue.notifications-sticky { + .notifications-sticky-item { + background-color: initial; + background: linear-gradient(to top, #ca2541 0%, #d55268 100%); + .icon { + display: inline-block; + vertical-align: bottom; + line-height: 16px; + height: 100%; + margin-right: 9px; + + img { + vertical-align: initial; + } + } + } +} + diff --git a/internal_packages/preferences/lib/tabs/preferences-account-details.jsx b/internal_packages/preferences/lib/tabs/preferences-account-details.jsx index c03c8693f..ea48bfbb6 100644 --- a/internal_packages/preferences/lib/tabs/preferences-account-details.jsx +++ b/internal_packages/preferences/lib/tabs/preferences-account-details.jsx @@ -1,7 +1,7 @@ import _ from 'underscore'; import React, {Component, PropTypes} from 'react'; import {EditableList, NewsletterSignup} from 'nylas-component-kit'; -import {RegExpUtils} from 'nylas-exports'; +import {RegExpUtils, Account} from 'nylas-exports'; class PreferencesAccountDetails extends Component { @@ -105,6 +105,15 @@ class PreferencesAccountDetails extends Component { this._setStateAndSave({defaultAlias}); }; + _reconnect() { + const {account} = this.state; + const ipc = require('electron').ipcRenderer; + ipc.send('command', 'application:add-account', account.provider); + } + _contactSupport() { + const {shell} = require("electron"); + shell.openExternal("https://support.nylas.com/hc/en-us/requests/new"); + } // Renderers @@ -124,6 +133,37 @@ class PreferencesAccountDetails extends Component { } } + + _renderErrorDetail(message, buttonText, buttonAction) { + return (
+
{message}
+ {buttonText} +
) + } + _renderSyncErrorDetails() { + const {account} = this.state; + if (account.syncState !== Account.SYNC_STATE_RUNNING) { + switch (account.syncState) { + case Account.SYNC_STATE_AUTH_FAILED: + return this._renderErrorDetail( + `Nylas N1 can no longer authenticate with ${account.emailAddress}. The password or + authentication may have changed.`, + "Reconnect", + ()=>this._reconnect()); + case Account.SYNC_STATE_STOPPED: + return this._renderErrorDetail( + `The cloud sync for ${account.emailAddress} has been disabled. Please contact Nylas support.`, + "Contact support", + ()=>this._contactSupport()); + default: + return this._renderErrorDetail( + `Nylas encountered an error while syncing mail for ${account.emailAddress}. Contact Nylas support for details.`, + "Contact support", + ()=>this._contactSupport()); + } + } + } + render() { const {account} = this.state; const aliasPlaceholder = this._makeAlias( @@ -132,6 +172,7 @@ class PreferencesAccountDetails extends Component { return (
+ {this._renderSyncErrorDetails(account)}

Account Label

+ + ); } diff --git a/internal_packages/preferences/lib/tabs/preferences-account-list.jsx b/internal_packages/preferences/lib/tabs/preferences-account-list.jsx index 96c906c0b..aea022902 100644 --- a/internal_packages/preferences/lib/tabs/preferences-account-list.jsx +++ b/internal_packages/preferences/lib/tabs/preferences-account-list.jsx @@ -1,5 +1,7 @@ import React, {Component, PropTypes} from 'react'; import {RetinaImg, Flexbox, EditableList} from 'nylas-component-kit'; +import {Account} from 'nylas-exports'; +import classnames from 'classnames'; class PreferencesAccountList extends Component { @@ -12,23 +14,35 @@ class PreferencesAccountList extends Component { onRemoveAccount: PropTypes.func.isRequired, }; + _renderAccountStateIcon(account) { + if (account.syncState !== "running") { + return (
) + } + } + _renderAccount = (account)=> { const label = account.label; const accountSub = `${account.name || 'No name provided'} <${account.emailAddress}>`; + const syncError = account.syncState !== Account.SYNC_STATE_RUNNING; return (
-
{label}
+
+ {label} +
{accountSub} ({account.displayProvider()})
diff --git a/internal_packages/preferences/stylesheets/preferences-accounts.less b/internal_packages/preferences/stylesheets/preferences-accounts.less index f845c945a..a3b67fc4d 100644 --- a/internal_packages/preferences/stylesheets/preferences-accounts.less +++ b/internal_packages/preferences/stylesheets/preferences-accounts.less @@ -18,11 +18,16 @@ border-bottom: 1px solid @border-color-divider; } + .list-item:not(.selected) .sync-error { + color: @text-color-error; + } + .account-name { font-size: @font-size-large; cursor:default; overflow: hidden; text-overflow: ellipsis; + vertical-align: middle; } .account-subtext { @@ -52,6 +57,29 @@ height: 140px; } + .account-error-detail { + display: flex; + flex-direction: column; + background: linear-gradient(to top, #ca2541 0%, #d55268 100%); + + .action { + flex-shrink: 0; + background-color: rgba(0,0,0,0.15); + text-align: center; + padding: 3px @padding-base-horizontal; + color: @text-color-inverse + } + .action:hover { + background-color: rgba(255,255,255,0.15); + text-decoration:none; + } + .message { + flex-grow: 1; + padding: 3px @padding-base-horizontal; + color: @text-color-inverse + } + } + .newsletter { padding-top: @padding-base-vertical * 2; input[type=checkbox] { margin: 0; position: relative; top: 0; } diff --git a/internal_packages/worker-sync/lib/nylas-long-connection.coffee b/internal_packages/worker-sync/lib/nylas-long-connection.coffee index 3ead7cb4d..e76333d33 100644 --- a/internal_packages/worker-sync/lib/nylas-long-connection.coffee +++ b/internal_packages/worker-sync/lib/nylas-long-connection.coffee @@ -98,7 +98,7 @@ class NylasLongConnection @withCursor (cursor) => return if @state is NylasLongConnection.State.Ended console.log("Delta Connection: Starting for account #{@_accountId}, token #{token}, with cursor #{cursor}") - options = url.parse("#{@_api.APIRoot}/delta/streaming?cursor=#{cursor}&exclude_folders=false&exclude_metadata=false") + options = url.parse("#{@_api.APIRoot}/delta/streaming?cursor=#{cursor}&exclude_folders=false&exclude_metadata=false&exclude_account=false") options.auth = "#{token}:" if @_api.APIRoot.indexOf('https') is -1 diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee index e24499135..18cd7d973 100644 --- a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee +++ b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee @@ -84,6 +84,14 @@ class NylasSyncWorkerPool metadata = metadata.concat(_.values(deltas['metadata'])) delete deltas['metadata'] + # Remove any account deltas, which are only used to notify broken/fixed sync state + # on accounts + delete create['account'] + delete destroy['account'] + if modify['account'] + @_handleAccountDeltas(_.values(modify['account'])) + delete modify['account'] + # Apply all the deltas to create objects. Gets promises for handling # each type of model in the `create` hash, waits for them all to resolve. create[type] = NylasAPI._handleModelResponse(_.values(dict)) for type, dict of create @@ -139,6 +147,10 @@ class NylasSyncWorkerPool localMetadatum.version = metadatum.version t.persistModel(model) + _handleAccountDeltas: (deltas) => + for delta in deltas + Actions.updateAccount(delta.account_id, {syncState: delta.sync_state}) + _handleDeltaDeletion: (delta) => klass = NylasAPI._apiObjectToClassMap[delta.object] return unless klass diff --git a/src/flux/action-bridge.coffee b/src/flux/action-bridge.coffee index 893bead26..93a51132d 100644 --- a/src/flux/action-bridge.coffee +++ b/src/flux/action-bridge.coffee @@ -49,7 +49,7 @@ class ActionBridge # Observe all global actions and re-broadcast them to other windows Actions.globalActions.forEach (name) => - callback = => @onRebroadcast(TargetWindows.ALL, name, arguments) + callback = (args...) => @onRebroadcast(TargetWindows.ALL, name, args) Actions[name].listen(callback, @) # Observe the database store (possibly other stores in the future), and @@ -63,7 +63,7 @@ class ActionBridge # Observe all mainWindow actions fired in this window and re-broadcast # them to other windows so the central application stores can take action Actions.workWindowActions.forEach (name) => - callback = => @onRebroadcast(TargetWindows.WORK, name, arguments) + callback = (args...) => @onRebroadcast(TargetWindows.WORK, name, args) Actions[name].listen(callback, @) onIPCMessage: (event, initiatorId, name, json) => @@ -90,7 +90,7 @@ class ActionBridge else throw new Error("#{@initiatorId} received unknown action-bridge event: #{name}") - onRebroadcast: (target, name, args...) => + onRebroadcast: (target, name, args) => if Actions[name]?.firing Actions[name].firing = false return @@ -99,7 +99,7 @@ class ActionBridge args.forEach (arg) -> if arg instanceof Function throw new Error("ActionBridge cannot forward action argument of type `function` to work window.") - params.push(arg[0]) + params.push(arg) json = JSON.stringify(params, Utils.registeredObjectReplacer) diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index fa667efb4..2f39b49a7 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -171,7 +171,7 @@ class Actions Actions.updateAccount(account.id, {accountName: 'new'}) ``` ### - @updateAccount: ActionScopeWindow + @updateAccount: ActionScopeGlobal ### Public: Re-order the provided account in the account list. diff --git a/src/flux/models/account.coffee b/src/flux/models/account.coffee index 25c1a25a1..27664cf77 100644 --- a/src/flux/models/account.coffee +++ b/src/flux/models/account.coffee @@ -29,6 +29,11 @@ Section: Models ### class Account extends ModelWithMetadata + @SYNC_STATE_RUNNING = "running" + @SYNC_STATE_STOPPED = "stopped" + @SYNC_STATE_AUTH_FAILED = "invalid" + @SYNC_STATE_ERROR = "sync_error" + @attributes: _.extend {}, ModelWithMetadata.attributes, 'name': Attributes.String modelKey: 'name' @@ -58,10 +63,16 @@ class Account extends ModelWithMetadata modelKey: 'defaultAlias' jsonKey: 'default_alias' + 'syncState': Attributes.String + queryable: false + modelKey: 'syncState' + jsonKey: 'sync_state' + constructor: -> super @aliases ||= [] @label ||= @emailAddress + @syncState ||= "running" fromJSON: (json) -> json["label"] ||= json[@constructor.attributes['emailAddress'].jsonKey] diff --git a/src/flux/nylas-api.coffee b/src/flux/nylas-api.coffee index 0d4b85aab..46c966d92 100644 --- a/src/flux/nylas-api.coffee +++ b/src/flux/nylas-api.coffee @@ -127,12 +127,6 @@ class NylasAPI NylasEnv.config.onDidChange('env', @_onConfigChanged) @_onConfigChanged() - if NylasEnv.isMainWindow() - Actions.notificationActionTaken.listen ({notification, action}) -> - if action.id is '401:reconnect' - ipc = require('electron').ipcRenderer - ipc.send('command', 'application:add-account', action.provider) - _onConfigChanged: => prev = {@AppID, @APIRoot, @APITokens} @@ -245,17 +239,16 @@ class NylasAPI type: 'error' tag: '401' sticky: true - message: "Nylas N1 can no longer authenticate with #{email}. You - will not be able to send or receive mail. Please click - here to reconnect your account.", + message: "Action failed: There was an error syncing with #{email}. You + may not be able to send or receive mail.", icon: 'fa-sign-out' actions: [{ - default: true - dismisses: true - label: 'Reconnect' - provider: account?.provider ? "" - id: '401:reconnect' - }] + default: true + dismisses: true + label: 'Dismiss' + provider: account?.provider ? "" + id: '401:dismiss' + }] return Promise.resolve() diff --git a/src/flux/stores/account-store.coffee b/src/flux/stores/account-store.coffee index 78c2fcd74..448658320 100644 --- a/src/flux/stores/account-store.coffee +++ b/src/flux/stores/account-store.coffee @@ -61,6 +61,7 @@ class AccountStore extends NylasStore # Inbound Events _onUpdateAccount: (id, updated) => + return unless NylasEnv.isMainWindow() idx = _.findIndex @_accounts, (a) -> a.id is id account = @_accounts[idx] return if !account diff --git a/static/images/notification/icon-alert-onred@1x.png b/static/images/notification/icon-alert-onred@1x.png new file mode 100644 index 000000000..734d5d7fd Binary files /dev/null and b/static/images/notification/icon-alert-onred@1x.png differ diff --git a/static/images/notification/icon-alert-onred@2x.png b/static/images/notification/icon-alert-onred@2x.png new file mode 100644 index 000000000..f76b1a1bd Binary files /dev/null and b/static/images/notification/icon-alert-onred@2x.png differ diff --git a/static/images/preferences/providers/ic-accountsettings-error@1x.png b/static/images/preferences/providers/ic-accountsettings-error@1x.png new file mode 100644 index 000000000..f6733ccdb Binary files /dev/null and b/static/images/preferences/providers/ic-accountsettings-error@1x.png differ diff --git a/static/images/preferences/providers/ic-accountsettings-error@2x.png b/static/images/preferences/providers/ic-accountsettings-error@2x.png new file mode 100644 index 000000000..916d82e4e Binary files /dev/null and b/static/images/preferences/providers/ic-accountsettings-error@2x.png differ diff --git a/static/images/preferences/providers/ic-settings-account-error@2x.png b/static/images/preferences/providers/ic-settings-account-error@2x.png new file mode 100644 index 000000000..c396d2846 Binary files /dev/null and b/static/images/preferences/providers/ic-settings-account-error@2x.png differ