mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-04 20:13:11 +08:00
feat(sidebar-notifs) Create sidebar notifications to replace old bars
Summary: Move the old bar notifications to the sidebar, and only display one notification at a time using a priority-rating system. Remove all of the old notification infrastructure. Test Plan: Added specs, also reproduced notifications locally Reviewers: bengotow Reviewed By: bengotow Subscribers: juan Differential Revision: https://phab.nylas.com/D3310
This commit is contained in:
parent
aa7ef91b0b
commit
9e3c3c14cd
43 changed files with 948 additions and 834 deletions
|
@ -1,48 +0,0 @@
|
|||
{Actions, LaunchServices} = require 'nylas-exports'
|
||||
|
||||
NOTIF_ACTION_YES = 'mailto:set-default-yes'
|
||||
NOTIF_ACTION_NO = 'mailto:set-default-no'
|
||||
|
||||
NOTIF_SETTINGS_KEY = 'nylas.mailto.prompted-about-default'
|
||||
|
||||
module.exports =
|
||||
activate: (@state) ->
|
||||
@services = new LaunchServices()
|
||||
|
||||
return unless @services.available()
|
||||
|
||||
# We shouldn't ask if they've already said No
|
||||
return if NylasEnv.config.get(NOTIF_SETTINGS_KEY) is true
|
||||
|
||||
@services.isRegisteredForURLScheme 'mailto', (registered) =>
|
||||
# Prompt them to make Nylas their default client
|
||||
unless registered
|
||||
@_unlisten = Actions.notificationActionTaken.listen(@_onNotificationActionTaken, @)
|
||||
Actions.postNotification
|
||||
type: 'info',
|
||||
sticky: true
|
||||
message: "Thanks for trying out N1! Would you like to make it your default mail client?",
|
||||
icon: 'fa-inbox',
|
||||
actions: [{
|
||||
label: 'Not Now'
|
||||
dismisses: true
|
||||
id: NOTIF_ACTION_NO
|
||||
},{
|
||||
label: 'Yes'
|
||||
dismisses: true
|
||||
default: true
|
||||
id: NOTIF_ACTION_YES
|
||||
}]
|
||||
|
||||
deactivate: ->
|
||||
@_unlisten?()
|
||||
|
||||
serialize: -> @state
|
||||
|
||||
_onNotificationActionTaken: ({notification, action}) ->
|
||||
if action.id is NOTIF_ACTION_YES
|
||||
@services.registerForURLScheme 'mailto', (err) ->
|
||||
console.log(err) if err
|
||||
|
||||
if action.id is NOTIF_ACTION_NO
|
||||
NylasEnv.config.set(NOTIF_SETTINGS_KEY, true)
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "notification-mailto",
|
||||
"main": "./lib/main",
|
||||
"version": "0.36.0",
|
||||
"description": "Displays a notification asking the user to make N1 their default mailto handler",
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"nylas": "*"
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
{Actions} = require 'nylas-exports'
|
||||
{ipcRenderer, remote} = require('electron')
|
||||
|
||||
module.exports =
|
||||
|
||||
activate: (@state) ->
|
||||
# Populate our initial state directly from the auto update manager.
|
||||
updater = remote.getGlobal('application').autoUpdateManager
|
||||
@_unlisten = Actions.notificationActionTaken.listen(@_onNotificationActionTaken, @)
|
||||
|
||||
configVersion = NylasEnv.config.get("lastVersion")
|
||||
currentVersion = NylasEnv.getVersion()
|
||||
if configVersion and configVersion isnt currentVersion
|
||||
NylasEnv.config.set("lastVersion", currentVersion)
|
||||
@displayThanksNotification()
|
||||
|
||||
if updater.getState() is 'update-available'
|
||||
@displayNotification(updater.releaseVersion)
|
||||
|
||||
NylasEnv.onUpdateAvailable ({releaseVersion, releaseNotes} = {}) =>
|
||||
@displayNotification(releaseVersion)
|
||||
|
||||
displayThanksNotification: ->
|
||||
Actions.postNotification
|
||||
type: 'info'
|
||||
tag: 'app-update'
|
||||
sticky: true
|
||||
message: "You're running the latest version of N1 - view the changelog to see what's new.",
|
||||
icon: 'fa-magic'
|
||||
actions: [{
|
||||
dismisses: true
|
||||
label: 'Thanks'
|
||||
id: 'release-bar:no-op'
|
||||
},{
|
||||
default: true
|
||||
dismisses: true
|
||||
label: 'See What\'s New'
|
||||
id: 'release-bar:view-changelog'
|
||||
}]
|
||||
|
||||
displayNotification: (version) ->
|
||||
version = if version then "(#{version})" else ''
|
||||
Actions.postNotification
|
||||
type: 'info'
|
||||
tag: 'app-update'
|
||||
sticky: true
|
||||
message: "An update to N1 is available #{version} - click to update now!",
|
||||
icon: 'fa-flag'
|
||||
actions: [{
|
||||
label: 'See What\'s New'
|
||||
id: 'release-bar:view-changelog'
|
||||
},{
|
||||
label: 'Install Now'
|
||||
dismisses: true
|
||||
default: true
|
||||
id: 'release-bar:install-update'
|
||||
}]
|
||||
|
||||
deactivate: ->
|
||||
@_unlisten()
|
||||
|
||||
_onNotificationActionTaken: ({notification, action}) ->
|
||||
if action.id is 'release-bar:install-update'
|
||||
ipcRenderer.send 'command', 'application:install-update'
|
||||
true
|
||||
if action.id is 'release-bar:view-changelog'
|
||||
require('electron').shell.openExternal('https://github.com/nylas/N1/blob/master/CHANGELOG.md')
|
||||
false
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"name": "notification-update-available",
|
||||
"main": "./lib/main",
|
||||
"version": "0.36.0",
|
||||
"description": "Displays a notification when an update is available",
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"nylas": "*"
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
proxyquire = require 'proxyquire'
|
||||
Reflux = require 'reflux'
|
||||
{Actions} = require 'nylas-exports'
|
||||
|
||||
stubUpdaterState = null
|
||||
stubUpdaterReleaseVersion = null
|
||||
ipcSendArgs = null
|
||||
|
||||
PackageMain = proxyquire "../lib/main",
|
||||
"electron":
|
||||
"ipcRenderer":
|
||||
send: ->
|
||||
ipcSendArgs = arguments
|
||||
"remote":
|
||||
getGlobal: (global) ->
|
||||
autoUpdateManager:
|
||||
releaseVersion: stubUpdaterReleaseVersion
|
||||
getState: -> stubUpdaterState
|
||||
|
||||
describe "NotificationUpdateAvailable", ->
|
||||
beforeEach ->
|
||||
stubUpdaterState = 'idle'
|
||||
stubUpdaterReleaseVersion = undefined
|
||||
ipcSendArgs = null
|
||||
@package = PackageMain
|
||||
|
||||
afterEach ->
|
||||
@package.deactivate()
|
||||
|
||||
describe "activate", ->
|
||||
it "should display a notification immediately if one is available", ->
|
||||
spyOn(@package, 'displayNotification')
|
||||
stubUpdaterState = 'update-available'
|
||||
@package.activate()
|
||||
expect(@package.displayNotification).toHaveBeenCalled()
|
||||
|
||||
it "should not display a notification if no update is avialable", ->
|
||||
spyOn(@package, 'displayNotification')
|
||||
stubUpdaterState = 'no-update-available'
|
||||
@package.activate()
|
||||
expect(@package.displayNotification).not.toHaveBeenCalled()
|
||||
|
||||
it "should listen for `window:update-available`", ->
|
||||
spyOn(NylasEnv, 'onUpdateAvailable').andCallThrough()
|
||||
@package.activate()
|
||||
expect(NylasEnv.onUpdateAvailable).toHaveBeenCalled()
|
||||
|
||||
describe "displayNotification", ->
|
||||
beforeEach ->
|
||||
@package.activate()
|
||||
|
||||
it "should fire a postNotification Action", ->
|
||||
spyOn(Actions, 'postNotification')
|
||||
@package.displayNotification()
|
||||
expect(Actions.postNotification).toHaveBeenCalled()
|
||||
|
||||
it "should include the version if one is provided", ->
|
||||
spyOn(Actions, 'postNotification')
|
||||
|
||||
version = '0.515.0-123123'
|
||||
@package.displayNotification(version)
|
||||
expect(Actions.postNotification).toHaveBeenCalled()
|
||||
|
||||
notifOptions = Actions.postNotification.mostRecentCall.args[0]
|
||||
expect(notifOptions.message.indexOf(version) > 0).toBe(true)
|
||||
|
||||
describe "when the action is taken", ->
|
||||
it "should fire the `application:install-update` IPC event", ->
|
||||
Actions.notificationActionTaken({notification: {}, action: {id: 'release-bar:install-update'}})
|
||||
expect(Array.prototype.slice.call(ipcSendArgs)).toEqual(['command', 'application:install-update'])
|
|
@ -1,25 +0,0 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.release-bar {
|
||||
order: -1;
|
||||
}
|
||||
.release-bar-inner {
|
||||
height: 25px;
|
||||
display: block;
|
||||
padding-left: 10px;
|
||||
padding-top: 2px;
|
||||
color: @text-color-inverse;
|
||||
background-color: @background-color-success;
|
||||
border-bottom: 1px solid darken(@background-color-success, 10%);
|
||||
|
||||
&:hover {
|
||||
background-color: darken(@background-color-success, 10%);
|
||||
border-bottom: 1px solid darken(@background-color-success, 15%);
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right:8px;
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
/* eslint global-require: 0 */
|
||||
import {AccountStore, Account, Actions, React, IdentityStore} from 'nylas-exports'
|
||||
import {RetinaImg} from 'nylas-component-kit'
|
||||
import {shell} from 'electron';
|
||||
|
||||
export default class AccountErrorHeader extends React.Component {
|
||||
static displayName = 'AccountErrorHeader';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = this.getStateFromStores();
|
||||
this.upgradeLabel = ""
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.unsubscribers = [
|
||||
AccountStore.listen(() => this.setState(this.getStateFromStores())),
|
||||
IdentityStore.listen(() => this.setState(this.getStateFromStores())),
|
||||
];
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
for (const unsub of this.unsubscribers) {
|
||||
unsub();
|
||||
}
|
||||
this.unsubscribers = null;
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
accounts: AccountStore.accounts(),
|
||||
subscriptionState: IdentityStore.subscriptionState(),
|
||||
daysUntilSubscriptionRequired: IdentityStore.daysUntilSubscriptionRequired(),
|
||||
}
|
||||
}
|
||||
|
||||
_reconnect(existingAccount) {
|
||||
const ipc = require('electron').ipcRenderer;
|
||||
ipc.send('command', 'application:add-account', {existingAccount});
|
||||
}
|
||||
|
||||
_openPreferences() {
|
||||
Actions.switchPreferencesTab('Accounts');
|
||||
Actions.openPreferences()
|
||||
}
|
||||
|
||||
_contactSupport() {
|
||||
shell.openExternal("https://support.nylas.com/hc/en-us/requests/new");
|
||||
}
|
||||
|
||||
_onCheckAgain = (event) => {
|
||||
this.setState({refreshing: true});
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
IdentityStore.refreshIdentityAndAccounts().finally(() => {
|
||||
if (!this.mounted) { return; }
|
||||
this.setState({refreshing: false});
|
||||
});
|
||||
}
|
||||
|
||||
_onUpgrade = () => {
|
||||
this.setState({buildingUpgradeURL: true});
|
||||
const isSubscription = this.state.subscriptionState === IdentityStore.State.Lapsed
|
||||
const utm = {
|
||||
source: "UpgradeBanner",
|
||||
campaign: isSubscription ? "SubscriptionExpired" : "TrialExpired",
|
||||
}
|
||||
IdentityStore.fetchSingleSignOnURL('/payment', utm).then((url) => {
|
||||
this.setState({buildingUpgradeURL: false});
|
||||
shell.openExternal(url);
|
||||
});
|
||||
}
|
||||
|
||||
_renderErrorHeader(message, buttonName, actionCallback) {
|
||||
return (
|
||||
<div className="account-error-header notifications-sticky">
|
||||
<div
|
||||
className={"notifications-sticky-item notification-error has-default-action"}
|
||||
onClick={actionCallback}
|
||||
>
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name="icon-alert-onred.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
<div className="message">
|
||||
{message}
|
||||
</div>
|
||||
<a className="action refresh" onClick={this._onCheckAgain}>
|
||||
{this.state.refreshing ? "Checking..." : "Check Again"}
|
||||
</a>
|
||||
<a className="action default" onClick={actionCallback}>
|
||||
{buttonName}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
_renderUpgradeHeader() {
|
||||
return (
|
||||
<div className="account-error-header notifications-sticky">
|
||||
<div
|
||||
className={"notifications-sticky-item notification-upgrade has-default-action"}
|
||||
onClick={this._onUpgrade}
|
||||
>
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name="ic-upgrade.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
<div className="message">
|
||||
{
|
||||
(this.state.subscriptionState === IdentityStore.State.Lapsed) ? (
|
||||
"Your subscription has expired, and we've paused your mailboxes. Renew your subscription to continue using N1!"
|
||||
) : (
|
||||
"Your trial has expired, and we've paused your mailboxes. Subscribe to continue using N1!"
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<a className="action refresh" onClick={this._onCheckAgain}>
|
||||
{this.state.refreshing ? "Checking..." : "Check Again"}
|
||||
</a>
|
||||
<a className="action default" onClick={this._onUpgrade}>
|
||||
{this.state.buildingUpgradeURL ? "Please wait..." : "Upgrade to Nylas Pro..."}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {accounts, subscriptionState, daysUntilSubscriptionRequired} = this.state;
|
||||
|
||||
const subscriptionInvalid = (subscriptionState !== IdentityStore.State.Valid);
|
||||
const subscriptionRequired = (daysUntilSubscriptionRequired !== null) && (daysUntilSubscriptionRequired <= 1);
|
||||
|
||||
if (subscriptionInvalid && subscriptionRequired) {
|
||||
return this._renderUpgradeHeader();
|
||||
}
|
||||
|
||||
const errorAccounts = accounts.filter(a => a.hasSyncStateError());
|
||||
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 <span />;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
React = require 'react'
|
||||
NotificationStore = require '../notifications-store'
|
||||
NotificationsItem = require './notifications-item'
|
||||
|
||||
class NotificationsHeader extends React.Component
|
||||
@displayName: "NotificationsHeader"
|
||||
|
||||
@containerRequired: false
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
_getStateFromStores: =>
|
||||
items: NotificationStore.stickyNotifications()
|
||||
|
||||
_onDataChanged: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@_unlistener = NotificationStore.listen(@_onDataChanged, @)
|
||||
@
|
||||
|
||||
# It's important that every React class explicitly stops listening to
|
||||
# N1 events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: =>
|
||||
@_unlistener() if @_unlistener
|
||||
@
|
||||
|
||||
render: =>
|
||||
<div className="notifications-sticky">
|
||||
{@_notificationComponents()}
|
||||
</div>
|
||||
|
||||
_notificationComponents: =>
|
||||
@state.items.map (notif) ->
|
||||
<NotificationsItem notification={notif} key={notif.message} />
|
||||
|
||||
|
||||
module.exports = NotificationsHeader
|
|
@ -1,39 +0,0 @@
|
|||
React = require 'react'
|
||||
{Actions} = require 'nylas-exports'
|
||||
|
||||
class NotificationsItem extends React.Component
|
||||
@displayName: "NotificationsItem"
|
||||
|
||||
render: =>
|
||||
notif = @props.notification
|
||||
iconClass = if notif.icon then "fa #{notif.icon}" else ""
|
||||
actionDefault = null
|
||||
actionComponents = notif.actions?.map (action) =>
|
||||
classname = "action "
|
||||
if action.default
|
||||
actionDefault = action
|
||||
classname += "default"
|
||||
|
||||
actionClick = (event) =>
|
||||
@_fireItemAction(notif, action)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
<a className={classname} key={action.label} onClick={actionClick}>
|
||||
{action.label}
|
||||
</a>
|
||||
|
||||
if actionDefault
|
||||
<div className={"notifications-sticky-item notification-#{notif.type} has-default-action"}
|
||||
onClick={=> @_fireItemAction(notif, actionDefault)}>
|
||||
<i className={iconClass}></i><div className="message">{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
else
|
||||
<div className={"notifications-sticky-item notification-#{notif.type}"}>
|
||||
<i className={iconClass}></i><div className="message">{notif.message}</div>{actionComponents}
|
||||
</div>
|
||||
|
||||
_fireItemAction: (notification, action) =>
|
||||
Actions.notificationActionTaken({notification, action})
|
||||
|
||||
module.exports = NotificationsItem
|
|
@ -0,0 +1,107 @@
|
|||
import {shell, ipcRenderer} from 'electron';
|
||||
import {React, Account, AccountStore, Actions, IdentityStore} from 'nylas-exports';
|
||||
import Notification from '../notification';
|
||||
|
||||
export default class AccountErrorNotification extends React.Component {
|
||||
static displayName = 'AccountErrorNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unlisten = AccountStore.listen(() => this.setState(this.getStateFromStores()));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
accounts: AccountStore.accounts(),
|
||||
}
|
||||
}
|
||||
|
||||
_onContactSupport = () => {
|
||||
shell.openExternal("https://support.nylas.com/hc/en-us/requests/new");
|
||||
}
|
||||
|
||||
_onReconnect = (existingAccount) => {
|
||||
ipcRenderer.send('command', 'application:add-account', {existingAccount});
|
||||
}
|
||||
|
||||
_onOpenAccountPreferences = () => {
|
||||
Actions.switchPreferencesTab('Accounts');
|
||||
Actions.openPreferences()
|
||||
}
|
||||
|
||||
_onCheckAgain = () => {
|
||||
return IdentityStore.refreshIdentityAndAccounts();
|
||||
}
|
||||
|
||||
render() {
|
||||
const erroredAccounts = this.state.accounts.filter(a => a.hasSyncStateError());
|
||||
let title;
|
||||
let subtitle;
|
||||
let subtitleAction;
|
||||
let actions;
|
||||
if (erroredAccounts.length === 0) {
|
||||
return <span />
|
||||
} else if (erroredAccounts.length > 1) {
|
||||
title = "Several of your accounts are having issues";
|
||||
actions = [{
|
||||
label: "Check Again",
|
||||
fn: this._onCheckAgain,
|
||||
}, {
|
||||
label: "Manage",
|
||||
fn: this._onOpenAccountPreferences,
|
||||
}];
|
||||
} else {
|
||||
const erroredAccount = erroredAccounts[0];
|
||||
switch (erroredAccount.syncState) {
|
||||
case Account.SYNC_STATE_AUTH_FAILED:
|
||||
title = `Cannot authenticate with ${erroredAccount.emailAddress}`;
|
||||
actions = [{
|
||||
label: "Check Again",
|
||||
fn: this._onCheckAgain,
|
||||
}, {
|
||||
label: 'Reconnect',
|
||||
fn: () => this._onReconnect(erroredAccount),
|
||||
}];
|
||||
break;
|
||||
case Account.SYNC_STATE_STOPPED:
|
||||
title = `Sync has been disabled for ${erroredAccount.emailAddress}`;
|
||||
subtitle = "Contact support";
|
||||
subtitleAction = this._onContactSupport;
|
||||
actions = [{
|
||||
label: "Check Again",
|
||||
fn: this._onCheckAgain,
|
||||
}];
|
||||
break;
|
||||
default:
|
||||
title = `Encountered an error with ${erroredAccount.emailAddress}`;
|
||||
subtitle = "Contact support";
|
||||
subtitleAction = this._onContactSupport;
|
||||
actions = [{
|
||||
label: "Check Again",
|
||||
fn: this._onCheckAgain,
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Notification
|
||||
priority="3"
|
||||
isError
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
subtitleAction={subtitleAction}
|
||||
actions={actions}
|
||||
icon="volstead-error.png"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import {React, LaunchServices} from 'nylas-exports';
|
||||
import Notification from '../notification';
|
||||
|
||||
const SETTINGS_KEY = 'nylas.mailto.prompted-about-default'
|
||||
|
||||
export default class DefaultClientNotification extends React.Component {
|
||||
static displayName = 'DefaultClientNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.services = new LaunchServices();
|
||||
this.state = this.getStateFromStores();
|
||||
this.state.initializing = true;
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.services.isRegisteredForURLScheme('mailto', (registered) => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
initializing: false,
|
||||
registered: registered,
|
||||
})
|
||||
}
|
||||
})
|
||||
this.disposable = NylasEnv.config.onDidChange(SETTINGS_KEY,
|
||||
() => this.setState(this.getStateFromStores()));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
alreadyPrompted: NylasEnv.config.get(SETTINGS_KEY),
|
||||
}
|
||||
}
|
||||
|
||||
_onAccept = () => {
|
||||
this.services.registerForURLScheme('mailto', (err) => {
|
||||
if (err) {
|
||||
NylasEnv.reportError(err)
|
||||
}
|
||||
});
|
||||
NylasEnv.config.set(SETTINGS_KEY, true)
|
||||
}
|
||||
|
||||
_onDecline = () => {
|
||||
NylasEnv.config.set(SETTINGS_KEY, true)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.initializing || this.state.alreadyPrompted || this.state.registered) {
|
||||
return <span />
|
||||
}
|
||||
return (
|
||||
<Notification
|
||||
title="Would you like to make N1 your default mail client?"
|
||||
priority="1"
|
||||
icon="volstead-defaultclient.png"
|
||||
actions={[{
|
||||
label: "Yes",
|
||||
fn: this._onAccept,
|
||||
}, {
|
||||
label: "No",
|
||||
fn: this._onDecline,
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
28
internal_packages/notifications/lib/items/dev-mode-notif.jsx
Normal file
28
internal_packages/notifications/lib/items/dev-mode-notif.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {React} from 'nylas-exports';
|
||||
import Notification from '../notification';
|
||||
|
||||
export default class DevModeNotification extends React.Component {
|
||||
static displayName = 'DevModeNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Don't need listeners to update this, since toggling dev mode reloads
|
||||
// the entire window anyway
|
||||
this.state = {
|
||||
inDevMode: NylasEnv.inDevMode(),
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.inDevMode) {
|
||||
return <span />
|
||||
}
|
||||
return (
|
||||
<Notification
|
||||
priority="0"
|
||||
title="N1 is running in dev mode!"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {React, MailRulesStore, Actions} from 'nylas-exports';
|
||||
import Notification from '../notification';
|
||||
|
||||
export default class DisabledMailRulesNotification extends React.Component {
|
||||
static displayName = 'DisabledMailRulesNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unlisten = MailRulesStore.listen(() => this.setState(this.getStateFromStores()));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
disabledRules: MailRulesStore.disabledRules(),
|
||||
}
|
||||
}
|
||||
|
||||
_onOpenMailRulesPreferences = () => {
|
||||
Actions.switchPreferencesTab('Mail Rules', {accountId: this.state.disabledRules[0].accountId})
|
||||
Actions.openPreferences()
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.disabledRules.length === 0) {
|
||||
return <span />
|
||||
}
|
||||
return (
|
||||
<Notification
|
||||
priority="2"
|
||||
title="One or more of your mail rules have been disabled."
|
||||
icon="volstead-defaultclient.png"
|
||||
isError
|
||||
actions={[{
|
||||
label: 'View Mail Rules',
|
||||
fn: this._onOpenMailRulesPreferences,
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,9 @@
|
|||
import {
|
||||
NylasAPI,
|
||||
NylasSyncStatusStore,
|
||||
React,
|
||||
Actions,
|
||||
} from 'nylas-exports';
|
||||
import {RetinaImg} from 'nylas-component-kit';
|
||||
import {NylasSyncStatusStore, React, Actions} from 'nylas-exports';
|
||||
import Notification from '../notification';
|
||||
|
||||
export default class ConnectionStatusHeader extends React.Component {
|
||||
static displayName = 'ConnectionStatusHeader';
|
||||
export default class OfflineNotification extends React.Component {
|
||||
static displayName = 'OfflineNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -16,7 +12,7 @@ export default class ConnectionStatusHeader extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = NylasSyncStatusStore.listen(() => {
|
||||
this.unlisten = NylasSyncStatusStore.listen(() => {
|
||||
const nextState = this.getStateFromStores();
|
||||
if ((nextState.connected !== this.state.connected) || (nextState.nextRetryText !== this.state.nextRetryText)) {
|
||||
this.setState(nextState);
|
||||
|
@ -68,6 +64,7 @@ export default class ConnectionStatusHeader extends React.Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {connected, nextRetryText};
|
||||
}
|
||||
|
||||
|
@ -86,29 +83,18 @@ export default class ConnectionStatusHeader extends React.Component {
|
|||
|
||||
render() {
|
||||
const {connected, nextRetryText} = this.state;
|
||||
|
||||
if (connected) {
|
||||
return (<span />);
|
||||
return <span />
|
||||
}
|
||||
|
||||
const apiDomain = NylasAPI.APIRoot.split('//').pop();
|
||||
|
||||
return (
|
||||
<div className="connection-status-header notifications-sticky">
|
||||
<div className={"notifications-sticky-item notification-offline"}>
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name="icon-alert-onred.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/>
|
||||
<div className="message">
|
||||
Nylas N1 isn't able to reach {apiDomain}. Retrying {nextRetryText}.
|
||||
</div>
|
||||
<a className="action default" onClick={this.onTryAgain}>
|
||||
Try Again Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<Notification
|
||||
title="Nylas N1 is offline"
|
||||
priority="5"
|
||||
icon="volstead-offline.png"
|
||||
subtitle={`Trying again ${nextRetryText}`}
|
||||
actions={[{label: 'Try now', id: 'try_now', fn: this.onTryAgain}]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import {React} from 'nylas-exports';
|
||||
import {ipcRenderer, remote, shell} from 'electron';
|
||||
import Notification from '../notification';
|
||||
|
||||
export default class UpdateNotification extends React.Component {
|
||||
static displayName = 'UpdateNotification';
|
||||
static containerRequired = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.updater = remote.getGlobal('application').autoUpdateManager
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.disposable = NylasEnv.onUpdateAvailable(() => {
|
||||
this.setState(this.getStateFromStores())
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
getStateFromStores() {
|
||||
return {
|
||||
updateAvailable: this.updater.getState() === 'update-available',
|
||||
version: this.updater.releaseVersion,
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate = () => {
|
||||
ipcRenderer.send('command', 'application:install-update')
|
||||
}
|
||||
|
||||
_onViewChangelog = () => {
|
||||
shell.openExternal('https://github.com/nylas/N1/blob/master/CHANGELOG.md')
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.updateAvailable) {
|
||||
return <span />
|
||||
}
|
||||
const version = this.state.version ? `(${this.state.version})` : '';
|
||||
return (
|
||||
<Notification
|
||||
priority="4"
|
||||
title={`An update to N1 is available ${version}`}
|
||||
subtitle="View changelog"
|
||||
subtitleAction={this._onViewChangelog}
|
||||
icon="volstead-upgrade.png"
|
||||
actions={[{
|
||||
label: "Update",
|
||||
fn: this._onUpdate,
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,26 +2,43 @@
|
|||
|
||||
import {ComponentRegistry, WorkspaceStore} from 'nylas-exports';
|
||||
import ActivitySidebar from "./sidebar/activity-sidebar";
|
||||
import NotificationStore from './notifications-store';
|
||||
import ConnectionStatusHeader from './headers/connection-status-header';
|
||||
import AccountErrorHeader from './headers/account-error-header';
|
||||
import NotificationsHeader from "./headers/notifications-header";
|
||||
import TrialRemainingBlock from "./sidebar/trial-remaining-block";
|
||||
import NotifWrapper from "./notif-wrapper";
|
||||
|
||||
import AccountErrorNotification from "./items/account-error-notif";
|
||||
import DefaultClientNotification from "./items/default-client-notif";
|
||||
import DevModeNotification from "./items/dev-mode-notif";
|
||||
import DisabledMailRulesNotification from "./items/disabled-mail-rules-notif";
|
||||
import OfflineNotification from "./items/offline-notification";
|
||||
import UpdateNotification from "./items/update-notification";
|
||||
|
||||
const notifications = [
|
||||
AccountErrorNotification,
|
||||
DefaultClientNotification,
|
||||
DevModeNotification,
|
||||
DisabledMailRulesNotification,
|
||||
OfflineNotification,
|
||||
UpdateNotification,
|
||||
]
|
||||
|
||||
export function activate() {
|
||||
ComponentRegistry.register(ActivitySidebar, {location: WorkspaceStore.Location.RootSidebar});
|
||||
ComponentRegistry.register(NotificationsHeader, {location: WorkspaceStore.Sheet.Global.Header});
|
||||
ComponentRegistry.register(ConnectionStatusHeader, {location: WorkspaceStore.Sheet.Global.Header});
|
||||
ComponentRegistry.register(AccountErrorHeader, {location: WorkspaceStore.Sheet.Threads.Header});
|
||||
ComponentRegistry.register(NotifWrapper, {location: WorkspaceStore.Location.RootSidebar});
|
||||
ComponentRegistry.register(TrialRemainingBlock, {location: WorkspaceStore.Location.RootSidebar});
|
||||
|
||||
for (const notification of notifications) {
|
||||
ComponentRegistry.register(notification, {role: 'RootSidebar:Notifications'});
|
||||
}
|
||||
}
|
||||
|
||||
export function serialize() {}
|
||||
|
||||
export function deactivate() {
|
||||
ComponentRegistry.unregister(ActivitySidebar);
|
||||
ComponentRegistry.unregister(NotificationsHeader);
|
||||
ComponentRegistry.unregister(ConnectionStatusHeader);
|
||||
ComponentRegistry.unregister(AccountErrorHeader);
|
||||
ComponentRegistry.unregister(TrialRemainingBlock);
|
||||
ComponentRegistry.unregister(NotifWrapper);
|
||||
|
||||
for (const notification of notifications) {
|
||||
ComponentRegistry.unregister(notification)
|
||||
}
|
||||
}
|
||||
|
|
49
internal_packages/notifications/lib/notif-wrapper.jsx
Normal file
49
internal_packages/notifications/lib/notif-wrapper.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import _ from 'underscore';
|
||||
import {React, ReactDOM} from 'nylas-exports'
|
||||
import {InjectedComponentSet} from 'nylas-component-kit'
|
||||
|
||||
const ROLE = "RootSidebar:Notifications";
|
||||
|
||||
export default class NotifWrapper extends React.Component {
|
||||
static displayName = 'NotifWrapper';
|
||||
|
||||
componentDidMount() {
|
||||
this.observer = new MutationObserver(this.update);
|
||||
this.observer.observe(ReactDOM.findDOMNode(this), {childList: true})
|
||||
this.update() // Necessary if notifications are already mounted
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
update = () => {
|
||||
const className = "highest-priority";
|
||||
const node = ReactDOM.findDOMNode(this);
|
||||
|
||||
const oldHighestPriorityElems = node.querySelectorAll(`.${className}`);
|
||||
for (const oldElem of oldHighestPriorityElems) {
|
||||
oldElem.classList.remove(className)
|
||||
}
|
||||
|
||||
const elemsWithPriority = node.querySelectorAll("[data-priority]")
|
||||
if (elemsWithPriority.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const highestPriorityElem = _.max(elemsWithPriority,
|
||||
(elem) => parseInt(elem.dataset.priority, 10))
|
||||
|
||||
highestPriorityElem.classList.add(className);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<InjectedComponentSet
|
||||
className="notifications"
|
||||
matching={{role: ROLE}}
|
||||
direction="column"
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
89
internal_packages/notifications/lib/notification.jsx
Normal file
89
internal_packages/notifications/lib/notification.jsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {React} from 'nylas-exports';
|
||||
import {RetinaImg} from 'nylas-component-kit';
|
||||
|
||||
export default class Notification extends React.Component {
|
||||
static containerRequired = false;
|
||||
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
subtitle: React.PropTypes.string,
|
||||
subtitleAction: React.PropTypes.func,
|
||||
actions: React.PropTypes.array,
|
||||
icon: React.PropTypes.string,
|
||||
priority: React.PropTypes.string,
|
||||
isError: React.PropTypes.bool,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {loadingActions: []}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
_onClick(actionId, actionFn) {
|
||||
const result = actionFn();
|
||||
if (result instanceof Promise) {
|
||||
this.setState({
|
||||
loadingActions: this.state.loadingActions.concat([actionId]),
|
||||
})
|
||||
|
||||
result.finally(() => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
loadingActions: this.state.loadingActions.filter(f => f !== actionId),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const actions = this.props.actions || [];
|
||||
const actionElems = actions.map((action, idx) => {
|
||||
const id = `action-${idx}`;
|
||||
let className = 'action'
|
||||
if (this.state.loadingActions.includes(id)) {
|
||||
className += ' loading'
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
id={id}
|
||||
className={className}
|
||||
onClick={() => this._onClick(id, action.fn)}
|
||||
>
|
||||
{action.label}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
const {isError, priority, icon, title, subtitleAction, subtitle} = this.props;
|
||||
return (
|
||||
<div className={`notification${isError ? ' error' : ''}`} data-priority={priority}>
|
||||
<div className="title">
|
||||
<RetinaImg
|
||||
className="icon"
|
||||
name={icon}
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
/> {title} <br />
|
||||
<span
|
||||
className={`subtitle ${subtitleAction ? 'has-action' : ''}`}
|
||||
onClick={subtitleAction || (() => {})}
|
||||
>
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
{actionElems.length > 0 ?
|
||||
<div className="actions-wrapper">{actionElems}</div> : null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
{Actions} = require 'nylas-exports'
|
||||
NylasStore = require 'nylas-store'
|
||||
|
||||
VERBOSE = false
|
||||
DISPLAY_TIME = 3000 # in ms
|
||||
|
||||
uuid_count = 0
|
||||
|
||||
class Notification
|
||||
constructor: ({@message, @type, @tag, @sticky, @actions, @icon} = {}) ->
|
||||
# Check to make sure the provided data is a valid notificaiton, since
|
||||
# notifications may be constructed by anyone developing on N1
|
||||
throw new Error "No `new` keyword when constructing Notification" unless @ instanceof Notification
|
||||
throw new Error "Type must be `info`, `developer`, `error`, or `success`" unless @type in ['info', 'error', 'success', 'developer']
|
||||
throw new Error "Message must be provided for notification" unless @message
|
||||
if @actions
|
||||
for action in @actions
|
||||
throw new Error "Actions must have an `label`" unless action['label']
|
||||
throw new Error "Actions must have an `id`" unless action['id']
|
||||
|
||||
@tag ?= uuid_count++
|
||||
@creation = Date.now()
|
||||
@sticky ?= false
|
||||
unless @sticky
|
||||
@expiry = @creation + DISPLAY_TIME
|
||||
|
||||
console.log "Created new notif with #{@tag}: #{@message}" if VERBOSE
|
||||
@
|
||||
|
||||
valid: ->
|
||||
@sticky or @expiry > Date.now()
|
||||
|
||||
toString: ->
|
||||
"Notification.#{@constructor.name}(#{@tag})"
|
||||
|
||||
class NotificationStore extends NylasStore
|
||||
constructor: ->
|
||||
@_flush()
|
||||
|
||||
# The notification store listens for user interaction with notififcations
|
||||
# and just removes the notifications. To implement notification actions,
|
||||
# your package should listen to notificationActionTaken and check the
|
||||
# notification and action objects.
|
||||
@listenTo Actions.notificationActionTaken, ({notification, action}) =>
|
||||
@_removeNotification(notification) if action.dismisses
|
||||
@listenTo Actions.postNotification, (data) =>
|
||||
@_postNotification(new Notification(data))
|
||||
@listenTo Actions.dismissNotificationsMatching, (criteria) =>
|
||||
for tag, notif of @_notifications
|
||||
if _.isMatch(notif, criteria)
|
||||
delete @_notifications[tag]
|
||||
@trigger()
|
||||
|
||||
######### PUBLIC #######################################################
|
||||
|
||||
notifications: =>
|
||||
console.log(JSON.stringify(@_notifications)) if VERBOSE
|
||||
sorted = _.sortBy(_.values(@_notifications), (n) -> -1*(n.creation + n.tag))
|
||||
_.reject sorted, (n) -> n.sticky
|
||||
|
||||
stickyNotifications: =>
|
||||
console.log(JSON.stringify(@_notifications)) if VERBOSE
|
||||
sorted = _.sortBy(_.values(@_notifications), (n) -> -1*(n.creation + n.tag))
|
||||
_.filter sorted, (n) -> n.sticky
|
||||
|
||||
Notification: Notification
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_flush: =>
|
||||
@_notifications = {}
|
||||
|
||||
_postNotification: (notification) =>
|
||||
console.log "Queue Notification.#{notification}" if VERBOSE
|
||||
@_notifications[notification.tag] = notification
|
||||
if notification.expiry?
|
||||
timeoutVal = Math.max(0, notification.expiry - Date.now())
|
||||
timeoutId = setTimeout =>
|
||||
@_removeNotification(notification)
|
||||
, timeoutVal
|
||||
notification.timeoutId = timeoutId
|
||||
|
||||
@trigger()
|
||||
|
||||
# Returns a function for removing a particular notification. See usage
|
||||
# above in setTimeout()
|
||||
_removeNotification: (notification) =>
|
||||
console.log "Removed #{notification}" if VERBOSE
|
||||
|
||||
clearTimeout(notification.timeoutId) if notification.timeoutId
|
||||
|
||||
delete @_notifications[notification.tag]
|
||||
@trigger()
|
||||
|
||||
# If the window matches the given context then we can show a
|
||||
# notification.
|
||||
_inWindowContext: (context={}) =>
|
||||
return true
|
||||
|
||||
module.exports = new NotificationStore()
|
|
@ -4,7 +4,6 @@ ReactCSSTransitionGroup = require 'react-addons-css-transition-group'
|
|||
_ = require 'underscore'
|
||||
classNames = require 'classnames'
|
||||
|
||||
NotificationStore = require '../notifications-store'
|
||||
StreamingSyncActivity = require './streaming-sync-activity'
|
||||
InitialSyncActivity = require './initial-sync-activity'
|
||||
|
||||
|
@ -28,14 +27,13 @@ class ActivitySidebar extends React.Component
|
|||
componentDidMount: =>
|
||||
@_unlisteners = []
|
||||
@_unlisteners.push TaskQueueStatusStore.listen @_onDataChanged
|
||||
@_unlisteners.push NotificationStore.listen @_onDataChanged
|
||||
@_unlisteners.push NylasSyncStatusStore.listen @_onDataChanged
|
||||
|
||||
componentWillUnmount: =>
|
||||
unlisten() for unlisten in @_unlisteners
|
||||
|
||||
render: =>
|
||||
items = [@_renderNotificationActivityItems(), @_renderTaskActivityItems()]
|
||||
items = @_renderTaskActivityItems()
|
||||
|
||||
if @state.isInitialSyncComplete
|
||||
items.push <StreamingSyncActivity key="streaming-sync" />
|
||||
|
@ -84,19 +82,10 @@ class ActivitySidebar extends React.Component
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_renderNotificationActivityItems: =>
|
||||
@state.notifications.map (notification) ->
|
||||
<div className="item" key={notification.id}>
|
||||
<div className="inner">
|
||||
{notification.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_onDataChanged: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: =>
|
||||
notifications: NotificationStore.notifications()
|
||||
tasks: TaskQueueStatusStore.queue()
|
||||
isInitialSyncComplete: NylasSyncStatusStore.isSyncComplete()
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {mount} from 'enzyme';
|
||||
import AccountErrorHeader from '../lib/headers/account-error-header';
|
||||
import {IdentityStore, AccountStore, Account, Actions, React} from 'nylas-exports'
|
||||
import AccountErrorNotification from '../lib/items/account-error-notif';
|
||||
import {IdentityStore, AccountStore, Account, Actions, React} from 'nylas-exports';
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
describe("AccountErrorHeader", function AccountErrorHeaderTests() {
|
||||
describe("AccountErrorNotif", function AccountErrorNotifTests() {
|
||||
describe("when one account is in the `invalid` state", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(AccountStore, 'accounts').andReturn([
|
||||
|
@ -13,22 +13,21 @@ describe("AccountErrorHeader", function AccountErrorHeaderTests() {
|
|||
});
|
||||
|
||||
it("renders an error bar that mentions the account email", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
expect(header.find('.notifications-sticky-item')).toBeDefined();
|
||||
expect(header.find('.message').text().indexOf('123@gmail.com') > 0).toBe(true);
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
expect(notif.find('.title').text().indexOf('123@gmail.com') > 0).toBe(true);
|
||||
});
|
||||
|
||||
it("allows the user to refresh the account", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
spyOn(IdentityStore, 'refreshIdentityAndAccounts').andReturn(Promise.resolve());
|
||||
header.find('.action.refresh').simulate('click');
|
||||
notif.find('#action-0').simulate('click'); // Expects first action to be the refresh action
|
||||
expect(IdentityStore.refreshIdentityAndAccounts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows the user to reconnect the account", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
spyOn(ipcRenderer, 'send');
|
||||
header.find('.action.default').simulate('click');
|
||||
notif.find('#action-1').simulate('click'); // Expects second action to be the reconnect action
|
||||
expect(ipcRenderer.send).toHaveBeenCalledWith('command', 'application:add-account', {
|
||||
existingAccount: AccountStore.accounts()[0],
|
||||
});
|
||||
|
@ -44,22 +43,22 @@ describe("AccountErrorHeader", function AccountErrorHeaderTests() {
|
|||
});
|
||||
|
||||
it("renders an error bar", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
expect(header.find('.notifications-sticky-item')).toBeDefined();
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
expect(notif.find('.notification').isEmpty()).toEqual(false);
|
||||
});
|
||||
|
||||
it("allows the user to refresh the accounts", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
spyOn(IdentityStore, 'refreshIdentityAndAccounts').andReturn(Promise.resolve());
|
||||
header.find('.action.refresh').simulate('click');
|
||||
notif.find('#action-0').simulate('click'); // Expects first action to be the refresh action
|
||||
expect(IdentityStore.refreshIdentityAndAccounts).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows the user to open preferences", () => {
|
||||
spyOn(Actions, 'switchPreferencesTab')
|
||||
spyOn(Actions, 'openPreferences')
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
header.find('.action.default').simulate('click');
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
notif.find('#action-1').simulate('click'); // Expects second action to be the preferences action
|
||||
expect(Actions.openPreferences).toHaveBeenCalled();
|
||||
expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Accounts');
|
||||
});
|
||||
|
@ -74,8 +73,8 @@ describe("AccountErrorHeader", function AccountErrorHeaderTests() {
|
|||
});
|
||||
|
||||
it("renders nothing", () => {
|
||||
const header = mount(<AccountErrorHeader />);
|
||||
expect(header.html()).toEqual('<span></span>');
|
||||
const notif = mount(<AccountErrorNotification />);
|
||||
expect(notif.find('.notification').isEmpty()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
import {mount} from 'enzyme';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {React} from 'nylas-exports';
|
||||
|
||||
let stubIsRegistered = null;
|
||||
let stubRegister = () => {};
|
||||
const patched = proxyquire('../lib/items/default-client-notif',
|
||||
{
|
||||
'nylas-exports': {
|
||||
LaunchServices: class {
|
||||
constructor() {
|
||||
this.isRegisteredForURLScheme = (urlScheme, callback) => { callback(stubIsRegistered) };
|
||||
this.registerForURLScheme = (urlScheme) => { stubRegister(urlScheme) };
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
const DefaultClientNotification = patched.default;
|
||||
const SETTINGS_KEY = 'nylas.mailto.prompted-about-default';
|
||||
|
||||
describe("DefaultClientNotif", function DefaultClientNotifTests() {
|
||||
describe("when N1 isn't the default mail client", () => {
|
||||
beforeEach(() => {
|
||||
stubIsRegistered = false;
|
||||
})
|
||||
describe("when the user has already responded", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasEnv.config, "get").andReturn(true);
|
||||
this.notif = mount(<DefaultClientNotification />);
|
||||
expect(NylasEnv.config.get).toHaveBeenCalledWith(SETTINGS_KEY);
|
||||
});
|
||||
it("renders nothing", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the user has yet to respond", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasEnv.config, "get").andReturn(false);
|
||||
this.notif = mount(<DefaultClientNotification />);
|
||||
expect(NylasEnv.config.get).toHaveBeenCalledWith(SETTINGS_KEY);
|
||||
});
|
||||
it("renders a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(false);
|
||||
});
|
||||
|
||||
it("allows the user to set N1 as the default client", () => {
|
||||
let scheme = null;
|
||||
stubRegister = (urlScheme) => { scheme = urlScheme };
|
||||
this.notif.find('#action-0').simulate('click'); // Expects first action to set N1 as default
|
||||
expect(scheme).toEqual('mailto');
|
||||
});
|
||||
|
||||
it("allows the user to decline", () => {
|
||||
spyOn(NylasEnv.config, "set")
|
||||
this.notif.find('#action-1').simulate('click'); // Expects second action to decline
|
||||
expect(NylasEnv.config.set).toHaveBeenCalledWith(SETTINGS_KEY, true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("when N1 is the default mail client", () => {
|
||||
beforeEach(() => {
|
||||
stubIsRegistered = true;
|
||||
this.notif = mount(<DefaultClientNotification />)
|
||||
})
|
||||
it("renders nothing", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(true);
|
||||
});
|
||||
})
|
||||
});
|
25
internal_packages/notifications/spec/dev-mode-notif-spec.jsx
Normal file
25
internal_packages/notifications/spec/dev-mode-notif-spec.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import {mount} from 'enzyme';
|
||||
import {React} from 'nylas-exports';
|
||||
import DevModeNotification from '../lib/items/dev-mode-notif';
|
||||
|
||||
describe("DevModeNotif", function DevModeNotifTests() {
|
||||
describe("When the window is in dev mode", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasEnv, "inDevMode").andReturn(true);
|
||||
this.notif = mount(<DevModeNotification />);
|
||||
})
|
||||
it("displays a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(false);
|
||||
})
|
||||
})
|
||||
|
||||
describe("When the window is not in dev mode", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasEnv, "inDevMode").andReturn(false);
|
||||
this.notif = mount(<DevModeNotification />);
|
||||
})
|
||||
it("doesn't display a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(true);
|
||||
})
|
||||
})
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import {mount} from 'enzyme';
|
||||
import {React, AccountStore, Account, Actions, MailRulesStore} from 'nylas-exports';
|
||||
import DisabledMailRulesNotification from '../lib/items/disabled-mail-rules-notif';
|
||||
|
||||
describe("DisabledMailRulesNotification", function DisabledMailRulesNotifTests() {
|
||||
beforeEach(() => {
|
||||
spyOn(AccountStore, 'accounts').andReturn([
|
||||
new Account({id: 'A', syncState: 'running', emailAddress: '123@gmail.com'}),
|
||||
])
|
||||
})
|
||||
describe("When there is one disabled mail rule", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(MailRulesStore, "disabledRules").andReturn([{accountId: 'A'}])
|
||||
this.notif = mount(<DisabledMailRulesNotification />)
|
||||
})
|
||||
it("displays a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(false);
|
||||
})
|
||||
|
||||
it("allows users to open the preferences", () => {
|
||||
spyOn(Actions, "switchPreferencesTab")
|
||||
spyOn(Actions, "openPreferences")
|
||||
this.notif.find('#action-0').simulate('click');
|
||||
expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Mail Rules', {accountId: 'A'})
|
||||
expect(Actions.openPreferences).toHaveBeenCalled();
|
||||
})
|
||||
});
|
||||
|
||||
describe("When there are multiple disabled mail rules", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(MailRulesStore, "disabledRules").andReturn([{accountId: 'A'},
|
||||
{accountId: 'A'}])
|
||||
this.notif = mount(<DisabledMailRulesNotification />)
|
||||
})
|
||||
it("displays a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(false);
|
||||
})
|
||||
|
||||
it("allows users to open the preferences", () => {
|
||||
spyOn(Actions, "switchPreferencesTab")
|
||||
spyOn(Actions, "openPreferences")
|
||||
this.notif.find('#action-0').simulate('click');
|
||||
expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Mail Rules', {accountId: 'A'})
|
||||
expect(Actions.openPreferences).toHaveBeenCalled();
|
||||
})
|
||||
});
|
||||
|
||||
describe("When there are no disabled mail rules", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(MailRulesStore, "disabledRules").andReturn([])
|
||||
this.notif = mount(<DisabledMailRulesNotification />)
|
||||
})
|
||||
it("does not display a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(true);
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,67 +0,0 @@
|
|||
NotificationsStore = require '../lib/notifications-store'
|
||||
Notification = NotificationsStore.Notification
|
||||
{Actions} = require 'nylas-exports'
|
||||
|
||||
describe 'Notification', ->
|
||||
|
||||
it 'should assert that a message has been provided', ->
|
||||
expect( -> new Notification({})).toThrow()
|
||||
|
||||
it 'should assert that a valid type has been provided', ->
|
||||
for type in ['info', 'success', 'error']
|
||||
expect( -> new Notification({type: type, message: 'bla'})).not.toThrow()
|
||||
expect( -> new Notification({type: 'extreme'})).toThrow()
|
||||
|
||||
it 'should assert that any actions have ids and labels', ->
|
||||
expect( -> new Notification({type: 'info', message: '1', actions:[{id: 'a'}]})).toThrow()
|
||||
expect( -> new Notification({type: 'info', message: '2', actions:[{label: 'b'}]})).toThrow()
|
||||
expect( -> new Notification({type: 'info', message: '3', actions:[{id: 'a', label: 'b'}]})).not.toThrow()
|
||||
|
||||
it 'should assign a tag and creation time', ->
|
||||
@n = new Notification({type: 'info', message: 'A', actions:[{id: 'a', label: 'b'}]})
|
||||
expect(@n.tag).toBeDefined()
|
||||
expect(@n.creation).toBeDefined()
|
||||
|
||||
it 'should use a provided tag if the notification is meant to replace an existing tag', ->
|
||||
@n = new Notification({tag: 'update', type: 'info', message: 'A', actions:[{id: 'a', label: 'b'}]})
|
||||
expect(@n.tag).toBe('update')
|
||||
|
||||
it 'should be valid at creation', ->
|
||||
@n = new Notification({type: 'info', message: 'A', actions:[{id: 'a', label: 'b'}]})
|
||||
expect(@n.valid()).toBe true
|
||||
|
||||
describe 'NotificationStore', ->
|
||||
beforeEach ->
|
||||
NotificationsStore._flush()
|
||||
|
||||
it 'should have no notifications by default', ->
|
||||
expect(NotificationsStore.notifications().length).toEqual 0
|
||||
|
||||
it 'should register a notification', ->
|
||||
message = "Hello"
|
||||
Actions.postNotification({type: 'info', message: message})
|
||||
n = NotificationsStore.notifications()[0]
|
||||
expect(n.message).toEqual(message)
|
||||
|
||||
it 'should unregister on removeNotification', ->
|
||||
Actions.postNotification({type: 'info', message: 'hi'})
|
||||
n = NotificationsStore.notifications()[0]
|
||||
NotificationsStore._removeNotification(n)
|
||||
expect(NotificationsStore.notifications().length).toEqual 0
|
||||
|
||||
describe "with a few notifications", ->
|
||||
beforeEach ->
|
||||
Actions.postNotification({type: 'info', message: 'A', sticky: true})
|
||||
Actions.postNotification({type: 'info', message: 'B', sticky: false})
|
||||
Actions.postNotification({type: 'info', message: 'C'})
|
||||
Actions.postNotification({type: 'info', message: 'D', sticky: true})
|
||||
|
||||
describe "stickyNotifications", ->
|
||||
it 'should return all of the notifications with the sticky flag, ordered by date DESC', ->
|
||||
messages = NotificationsStore.stickyNotifications().map (n) -> n.message
|
||||
expect(messages).toEqual(['D','A'])
|
||||
|
||||
describe "notifications", ->
|
||||
it 'should return all of the notifications without the sticky flag, ordered by date DESC', ->
|
||||
messages = NotificationsStore.notifications().map (n) -> n.message
|
||||
expect(messages).toEqual(['C','B'])
|
30
internal_packages/notifications/spec/offline-notif-spec.jsx
Normal file
30
internal_packages/notifications/spec/offline-notif-spec.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {mount} from 'enzyme';
|
||||
import {React, NylasSyncStatusStore} from 'nylas-exports';
|
||||
import OfflineNotification from '../lib/items/offline-notification';
|
||||
|
||||
describe("OfflineNotif", function offlineNotifTests() {
|
||||
describe("When N1 is offline", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasSyncStatusStore, "connected").andReturn(false);
|
||||
spyOn(NylasSyncStatusStore, "nextRetryDelay").andReturn(10000);
|
||||
this.notif = mount(<OfflineNotification />);
|
||||
})
|
||||
it("displays a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(false);
|
||||
})
|
||||
|
||||
it("allows the user to try connecting now", () => {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("When N1 is online", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasSyncStatusStore, "connected").andReturn(true);
|
||||
this.notif = mount(<OfflineNotification />);
|
||||
})
|
||||
it("doesn't display a notification", () => {
|
||||
expect(this.notif.find('.notification').isEmpty()).toEqual(true);
|
||||
})
|
||||
})
|
||||
});
|
65
internal_packages/notifications/spec/priority-spec.jsx
Normal file
65
internal_packages/notifications/spec/priority-spec.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {mount} from 'enzyme';
|
||||
import {ComponentRegistry, React} from 'nylas-exports';
|
||||
import NotifWrapper from '../lib/notif-wrapper.jsx';
|
||||
import Notification from '../lib/notification.jsx';
|
||||
|
||||
const stubNotif = (priority) => {
|
||||
return class extends React.Component {
|
||||
static displayName = `NotifPriority${priority}`;
|
||||
static containerRequired = false;
|
||||
render() { return <Notification priority={`${priority}`} title={`Priority ${priority}`} /> }
|
||||
}
|
||||
};
|
||||
|
||||
const checkHighestPriority = (expectedPriority, wrapper) => {
|
||||
const visibleElems = wrapper.find(".highest-priority")
|
||||
expect(visibleElems.isEmpty()).toEqual(false);
|
||||
const titleElem = visibleElems.first().find('.title');
|
||||
expect(titleElem.isEmpty()).toEqual(false);
|
||||
expect(titleElem.text().trim()).toEqual(`Priority ${expectedPriority}`);
|
||||
// Make sure there's only one highest-priority elem
|
||||
expect(visibleElems.get(1)).toEqual(undefined);
|
||||
}
|
||||
|
||||
describe("NotifPriority", function notifPriorityTests() {
|
||||
beforeEach(() => {
|
||||
this.wrapper = mount(<NotifWrapper />)
|
||||
this.trigger = () => {
|
||||
ComponentRegistry.trigger();
|
||||
this.wrapper.get(0).update();
|
||||
}
|
||||
})
|
||||
describe("When there is only one notification", () => {
|
||||
beforeEach(() => {
|
||||
ComponentRegistry._clear();
|
||||
ComponentRegistry.register(stubNotif(5), {role: 'RootSidebar:Notifications'})
|
||||
this.trigger();
|
||||
})
|
||||
it("should mark it as highest-priority", () => {
|
||||
checkHighestPriority(5, this.wrapper);
|
||||
})
|
||||
})
|
||||
describe("when there are multiple notifications", () => {
|
||||
beforeEach(() => {
|
||||
this.components = [stubNotif(5), stubNotif(7), stubNotif(3), stubNotif(2)]
|
||||
ComponentRegistry._clear();
|
||||
this.components.forEach((item) => {
|
||||
ComponentRegistry.register(item, {role: 'RootSidebar:Notifications'})
|
||||
})
|
||||
this.trigger();
|
||||
})
|
||||
it("should mark the proper one as highest-priority", () => {
|
||||
checkHighestPriority(7, this.wrapper);
|
||||
})
|
||||
it("properly updates when a highest-priority notification is removed", () => {
|
||||
ComponentRegistry.unregister(this.components[1])
|
||||
this.trigger();
|
||||
checkHighestPriority(5, this.wrapper);
|
||||
})
|
||||
it("properly updates when a higher priority notifcation is added", () => {
|
||||
ComponentRegistry.register(stubNotif(10), {role: 'RootSidebar:Notifications'});
|
||||
this.trigger();
|
||||
checkHighestPriority(10, this.wrapper);
|
||||
})
|
||||
})
|
||||
});
|
77
internal_packages/notifications/spec/update-notif-spec.jsx
Normal file
77
internal_packages/notifications/spec/update-notif-spec.jsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import {mount} from 'enzyme';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {React} from 'nylas-exports';
|
||||
|
||||
let stubUpdaterState = null
|
||||
let stubUpdaterReleaseVersion = null
|
||||
let ipcSendArgs = null
|
||||
|
||||
const patched = proxyquire("../lib/items/update-notification",
|
||||
{
|
||||
electron: {
|
||||
ipcRenderer: {
|
||||
send: (...args) => {
|
||||
ipcSendArgs = args
|
||||
},
|
||||
},
|
||||
remote: {
|
||||
getGlobal: () => {
|
||||
return {
|
||||
autoUpdateManager: {
|
||||
releaseVersion: stubUpdaterReleaseVersion,
|
||||
getState: () => stubUpdaterState,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const UpdateNotification = patched.default;
|
||||
|
||||
describe("UpdateNotification", () => {
|
||||
beforeEach(() => {
|
||||
stubUpdaterState = 'idle'
|
||||
stubUpdaterReleaseVersion = undefined
|
||||
ipcSendArgs = null
|
||||
})
|
||||
|
||||
describe("mounting", () => {
|
||||
it("should display a notification immediately if one is available", () => {
|
||||
stubUpdaterState = 'update-available'
|
||||
const notif = mount(<UpdateNotification />);
|
||||
expect(notif.find('.notification').isEmpty()).toEqual(false);
|
||||
})
|
||||
|
||||
it("should not display a notification if no update is avialable", () => {
|
||||
stubUpdaterState = 'no-update-available'
|
||||
const notif = mount(<UpdateNotification />);
|
||||
expect(notif.find('.notification').isEmpty()).toEqual(true);
|
||||
})
|
||||
|
||||
it("should listen for `window:update-available`", () => {
|
||||
spyOn(NylasEnv, 'onUpdateAvailable').andCallThrough()
|
||||
mount(<UpdateNotification />);
|
||||
expect(NylasEnv.onUpdateAvailable).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("displayNotification", () => {
|
||||
it("should include the version if one is provided", () => {
|
||||
stubUpdaterState = 'update-available'
|
||||
stubUpdaterReleaseVersion = '0.515.0-123123'
|
||||
const notif = mount(<UpdateNotification />);
|
||||
expect(notif.find('.title').text().indexOf('0.515.0-123123') >= 0).toBe(true);
|
||||
})
|
||||
|
||||
describe("when the action is taken", () => {
|
||||
it("should fire the `application:install-update` IPC event", () => {
|
||||
stubUpdaterState = 'update-available'
|
||||
const notif = mount(<UpdateNotification />);
|
||||
notif.find('#action-0').simulate('click'); // Expects the first action to be the install action
|
||||
expect(ipcSendArgs).toEqual(['command', 'application:install-update'])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
91
internal_packages/notifications/stylesheets/styles.less
Normal file
91
internal_packages/notifications/stylesheets/styles.less
Normal file
|
@ -0,0 +1,91 @@
|
|||
@import 'ui-variables';
|
||||
|
||||
.notifications {
|
||||
background-color: @panel-background-color;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: @background-color-info;
|
||||
display: none;
|
||||
color: @text-color-inverse;
|
||||
margin: 10px;
|
||||
border-radius: @border-radius-large;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: @background-color-error;
|
||||
}
|
||||
|
||||
.notification.highest-priority {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notif-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.notification .icon {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.notification .title {
|
||||
padding: 10px;
|
||||
padding-left: 40px;
|
||||
text-indent: -30px;
|
||||
}
|
||||
|
||||
.notification .subtitle {
|
||||
font-size: @font-size-smaller;
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.notification .subtitle.has-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification .subtitle.has-action::after {
|
||||
content:'';
|
||||
background: url(nylas://notifications/assets/minichevron@2x.png) top left no-repeat;
|
||||
background-size: 4.5px 7px;
|
||||
margin-left:3px;
|
||||
display: inline-block;
|
||||
width:4.5px;
|
||||
height:7px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.notification .actions-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.notification .action {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
border-top: solid rgba(255, 255, 255, 0.5) 1px;
|
||||
border-left: solid rgba(255, 255, 255, 0.5) 1px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
/* The semi-transparent backgrounds that can be layered on top
|
||||
of this class shouldn't have sharp corners on the bottom */
|
||||
border-bottom-left-radius: @border-radius-large;
|
||||
border-bottom-right-radius: @border-radius-large;
|
||||
}
|
||||
|
||||
.notification .action:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.notification .action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: @standard-shadow inset;
|
||||
}
|
||||
|
||||
.notification .action.loading {
|
||||
cursor: progress;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
box-shadow: @standard-shadow inset;
|
||||
}
|
|
@ -43,8 +43,6 @@ class ThreadListStore extends NylasStore
|
|||
# Inbound Events
|
||||
|
||||
_onPerspectiveChanged: =>
|
||||
if FocusedPerspectiveStore.current().searchQuery is undefined
|
||||
Actions.dismissNotificationsMatching({tag: 'search-error'})
|
||||
@createListDataSource()
|
||||
|
||||
_onDataChanged: ({previous, next} = {}) =>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
@import "darkside-variables";
|
||||
|
||||
.notifications-sticky .notifications-sticky-item {
|
||||
.notifications {
|
||||
background-color: @sidebar;
|
||||
}
|
||||
|
||||
.notifications .notification{
|
||||
background-color: @accent;
|
||||
line-height: 50px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
@import "variables";
|
||||
|
||||
.notifications-sticky, .notification-developer, .notifications-sticky-item, .notification-info {
|
||||
.notification {
|
||||
color: @white !important;
|
||||
background: @taiga-accent !important;
|
||||
|
||||
.action {
|
||||
color: @white !important;
|
||||
background: darken(@taiga-accent, 20%) !important;
|
||||
background: darken(@taiga-accent, 20%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {ipcRenderer} from 'electron'
|
|||
* erased!).
|
||||
*/
|
||||
|
||||
function onNotificationActionTaken(numAsks) {
|
||||
function onDialogActionTaken(numAsks) {
|
||||
return (buttonIndex) => {
|
||||
if (buttonIndex === 0) {
|
||||
ipcRenderer.send("move-to-applications")
|
||||
|
@ -74,7 +74,7 @@ export function activate() {
|
|||
detail: msg,
|
||||
defaultId: 0,
|
||||
cancelId: CANCEL_ID,
|
||||
}, onNotificationActionTaken(numAsks))
|
||||
}, onDialogActionTaken(numAsks))
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
|
|
|
@ -92,7 +92,6 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
spyOn(DBt, 'unpersistModel').andReturn(Promise.resolve());
|
||||
spyOn(DBt, 'persistModel').andReturn(Promise.resolve());
|
||||
spyOn(SoundRegistry, "playSound");
|
||||
spyOn(Actions, "postNotification");
|
||||
spyOn(Actions, "sendDraftSuccess");
|
||||
});
|
||||
|
||||
|
|
|
@ -32,8 +32,6 @@ how it propogates between windows.
|
|||
## Firing Actions
|
||||
|
||||
```coffee
|
||||
Actions.postNotification({message: "Removed Thread", type: 'success'})
|
||||
|
||||
Actions.queueTask(new ChangeStarredTask(thread: @_thread, starred: true))
|
||||
```
|
||||
|
||||
|
@ -413,58 +411,6 @@ class Actions
|
|||
###
|
||||
@RSVPEvent: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Fire to display an in-window notification to the user in the app's standard
|
||||
notification interface.
|
||||
|
||||
*Scope: Global*
|
||||
|
||||
```
|
||||
# A simple notification
|
||||
Actions.postNotification({message: "Removed Thread", type: 'success'})
|
||||
|
||||
# A sticky notification with actions
|
||||
NOTIF_ACTION_YES = 'YES'
|
||||
NOTIF_ACTION_NO = 'NO'
|
||||
|
||||
Actions.postNotification
|
||||
type: 'info',
|
||||
sticky: true
|
||||
message: "Thanks for trying out N1! Would you like to make it your default mail client?",
|
||||
icon: 'fa-inbox',
|
||||
actions: [{
|
||||
label: 'Yes'
|
||||
default: true
|
||||
dismisses: true
|
||||
id: NOTIF_ACTION_YES
|
||||
},{
|
||||
label: 'More Info'
|
||||
dismisses: false
|
||||
id: NOTIF_ACTION_MORE_INFO
|
||||
}]
|
||||
|
||||
```
|
||||
###
|
||||
@postNotification: ActionScopeGlobal
|
||||
|
||||
@dismissNotificationsMatching: ActionScopeGlobal
|
||||
|
||||
###
|
||||
Public: Listen to this action to handle user interaction with notifications you
|
||||
published via `postNotification`.
|
||||
|
||||
*Scope: Global*
|
||||
|
||||
```
|
||||
@_unlisten = Actions.notificationActionTaken.listen(@_onActionTaken, @)
|
||||
|
||||
_onActionTaken: ({notification, action}) ->
|
||||
if action.id is NOTIF_ACTION_YES
|
||||
# perform action
|
||||
```
|
||||
###
|
||||
@notificationActionTaken: ActionScopeGlobal
|
||||
|
||||
# FullContact Sidebar
|
||||
@getFullContactDetails: ActionScopeWindow
|
||||
@focusContact: ActionScopeWindow
|
||||
|
|
|
@ -26,7 +26,6 @@ class MailRulesStore extends NylasStore
|
|||
@listenTo Actions.reorderMailRule, @_onReorderMailRule
|
||||
@listenTo Actions.updateMailRule, @_onUpdateMailRule
|
||||
@listenTo Actions.disableMailRule, @_onDisableMailRule
|
||||
@listenTo Actions.notificationActionTaken, @_onNotificationActionTaken
|
||||
|
||||
rules: =>
|
||||
@_rules
|
||||
|
@ -34,6 +33,9 @@ class MailRulesStore extends NylasStore
|
|||
rulesForAccountId: (accountId) =>
|
||||
@_rules.filter (f) => f.accountId is accountId
|
||||
|
||||
disabledRules: (accountId) =>
|
||||
@_rules.filter (f) => f.disabled
|
||||
|
||||
_onDeleteMailRule: (id) =>
|
||||
@_rules = @_rules.filter (f) -> f.id isnt id
|
||||
@_saveMailRules()
|
||||
|
@ -74,22 +76,6 @@ class MailRulesStore extends NylasStore
|
|||
existing = _.find @_rules, (f) -> id is f.id
|
||||
return if not existing or existing.disabled is true
|
||||
|
||||
Actions.postNotification
|
||||
message: "We were unable to run your mail rules - one or more rules have been disabled."
|
||||
type: "error"
|
||||
tag: 'mail-rule-failure'
|
||||
sticky: true
|
||||
actions: [{
|
||||
label: 'Hide'
|
||||
dismisses: true
|
||||
id: 'hide'
|
||||
},{
|
||||
label: 'View Rules'
|
||||
dismisses: true
|
||||
default: true
|
||||
id: 'mail-rule-failure:view-rules'
|
||||
}]
|
||||
|
||||
# Disable the task
|
||||
existing.disabled = true
|
||||
existing.disabledReason = reason
|
||||
|
@ -101,20 +87,10 @@ class MailRulesStore extends NylasStore
|
|||
|
||||
@trigger()
|
||||
|
||||
_onNotificationActionTaken: ({notification, action}) =>
|
||||
return unless NylasEnv.isMainWindow()
|
||||
accountId = AccountStore.accounts()[0].accountId
|
||||
if action.id is 'mail-rule-failure:view-rules'
|
||||
Actions.switchPreferencesTab('Mail Rules', {accountId})
|
||||
Actions.openPreferences()
|
||||
|
||||
_saveMailRules: =>
|
||||
@_saveMailRulesDebounced ?= _.debounce =>
|
||||
DatabaseStore.inTransaction (t) =>
|
||||
t.persistJSONBlob(RulesJSONBlobKey, @_rules)
|
||||
|
||||
if not _.findWhere(@_rules, {disabled: true})
|
||||
Actions.dismissNotificationsMatching({tag: 'mail-rule-failure'})
|
||||
,1000
|
||||
@_saveMailRulesDebounced()
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import Task from './task';
|
|||
import {APIError} from '../errors';
|
||||
import Message from '../models/message';
|
||||
import DatabaseStore from '../stores/database-store';
|
||||
import Actions from '../actions';
|
||||
import NylasAPI from '../nylas-api';
|
||||
import BaseDraftTask from './base-draft-task';
|
||||
|
||||
|
@ -65,10 +64,7 @@ export default class DestroyDraftTask extends BaseDraftTask {
|
|||
return Promise.resolve(Task.Status.Retry);
|
||||
}
|
||||
|
||||
Actions.postNotification({
|
||||
message: "Unable to delete this draft. Restoring...",
|
||||
type: "error",
|
||||
});
|
||||
NylasEnv.showErrorDialog("Unable to delete this draft. Restoring...");
|
||||
|
||||
return DatabaseStore.inTransaction((t) =>
|
||||
t.persistModel(this.draft)
|
||||
|
|
|
@ -219,17 +219,7 @@ class WindowEventHandler
|
|||
showDevModeMessages: ->
|
||||
return unless NylasEnv.isMainWindow()
|
||||
|
||||
if NylasEnv.inDevMode()
|
||||
Actions = require './flux/actions'
|
||||
Actions.postNotification
|
||||
icon: 'fa-flask'
|
||||
type: 'developer'
|
||||
tag: 'developer'
|
||||
sticky: true
|
||||
actions: [{label: 'Thanks', id: 'ok', dismisses: true, default: true}]
|
||||
message: "N1 is running with debug flags enabled (slower). Packages in
|
||||
~/.nylas/dev/packages will be loaded. Have fun!"
|
||||
else
|
||||
if !NylasEnv.inDevMode()
|
||||
console.log("%c Welcome to N1! If you're exploring the source or building a
|
||||
plugin, you should enable debug flags. It's slower, but
|
||||
gives you better exceptions, the debug version of React,
|
||||
|
|
BIN
static/images/notification/volstead-defaultclient@2x.png
Normal file
BIN
static/images/notification/volstead-defaultclient@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
static/images/notification/volstead-error@2x.png
Normal file
BIN
static/images/notification/volstead-error@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
static/images/notification/volstead-offline@2x.png
Normal file
BIN
static/images/notification/volstead-offline@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
static/images/notification/volstead-salesforce@2x.png
Normal file
BIN
static/images/notification/volstead-salesforce@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
static/images/notification/volstead-upgrade@2x.png
Normal file
BIN
static/images/notification/volstead-upgrade@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in a new issue