mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-24 16:14:01 +08:00
parent
6910520dc5
commit
a5e3bf86c2
4 changed files with 560 additions and 419 deletions
|
@ -1,142 +0,0 @@
|
||||||
_ = require 'underscore'
|
|
||||||
{Thread,
|
|
||||||
Actions,
|
|
||||||
MailboxPerspective,
|
|
||||||
AccountStore,
|
|
||||||
CategoryStore,
|
|
||||||
SoundRegistry,
|
|
||||||
FocusedPerspectiveStore,
|
|
||||||
NativeNotifications,
|
|
||||||
DatabaseStore} = require 'nylas-exports'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
|
|
||||||
config:
|
|
||||||
enabled:
|
|
||||||
type: 'boolean'
|
|
||||||
default: true
|
|
||||||
|
|
||||||
activate: ->
|
|
||||||
@unlisteners = []
|
|
||||||
@unlisteners.push Actions.didPassivelyReceiveNewModels.listen(@_onNewMailReceived, @)
|
|
||||||
@activationTime = Date.now()
|
|
||||||
@stack = []
|
|
||||||
@stackProcessTimer = null
|
|
||||||
|
|
||||||
deactivate: ->
|
|
||||||
fn() for fn in @unlisteners
|
|
||||||
|
|
||||||
serialize: ->
|
|
||||||
|
|
||||||
_notifyAll: ->
|
|
||||||
NativeNotifications.displayNotification
|
|
||||||
title: "#{@stack.length} Unread Messages",
|
|
||||||
tag: 'unread-update'
|
|
||||||
@stack = []
|
|
||||||
|
|
||||||
_notifyOne: ({message, thread}) ->
|
|
||||||
account = AccountStore.accountForId(message.accountId)
|
|
||||||
from = message.from[0]?.displayName() ? "Unknown"
|
|
||||||
title = from
|
|
||||||
if message.subject and message.subject.length > 0
|
|
||||||
subtitle = message.subject
|
|
||||||
body = message.snippet
|
|
||||||
else
|
|
||||||
subtitle = message.snippet
|
|
||||||
body = null
|
|
||||||
|
|
||||||
notif = NativeNotifications.displayNotification
|
|
||||||
title: title
|
|
||||||
subtitle: subtitle
|
|
||||||
body: body
|
|
||||||
canReply: true
|
|
||||||
tag: 'unread-update'
|
|
||||||
onActivate: ({tag, response, activationType}) =>
|
|
||||||
if activationType is 'replied' and response and _.isString(response)
|
|
||||||
Actions.sendQuickReply({thread, message}, response)
|
|
||||||
else
|
|
||||||
NylasEnv.displayWindow()
|
|
||||||
|
|
||||||
currentCategories = FocusedPerspectiveStore.current().categories()
|
|
||||||
desiredCategory = thread.categoryNamed('inbox')
|
|
||||||
|
|
||||||
return unless desiredCategory
|
|
||||||
unless desiredCategory.id in _.pluck(currentCategories, 'id')
|
|
||||||
filter = MailboxPerspective.forCategory(desiredCategory)
|
|
||||||
Actions.focusMailboxPerspective(filter)
|
|
||||||
Actions.setFocus(collection: 'thread', item: thread)
|
|
||||||
|
|
||||||
_notifyMessages: ->
|
|
||||||
if @stack.length >= 5
|
|
||||||
@_notifyAll()
|
|
||||||
else if @stack.length > 0
|
|
||||||
@_notifyOne(@stack.pop())
|
|
||||||
|
|
||||||
@stackProcessTimer = null
|
|
||||||
if @stack.length > 0
|
|
||||||
@stackProcessTimer = setTimeout(( => @_notifyMessages()), 2000)
|
|
||||||
|
|
||||||
# https://phab.nylas.com/D2188
|
|
||||||
_onNewMessagesMissingThreads: (messages) ->
|
|
||||||
setTimeout =>
|
|
||||||
threads = {}
|
|
||||||
for msg in messages
|
|
||||||
threads[msg.threadId] ?= DatabaseStore.find(Thread, msg.threadId)
|
|
||||||
Promise.props(threads).then (threads) =>
|
|
||||||
messages = _.filter messages, (msg) =>
|
|
||||||
threads[msg.threadId]?
|
|
||||||
if messages.length > 0
|
|
||||||
@_onNewMailReceived({message: messages, thread: _.values(threads)})
|
|
||||||
, 10000
|
|
||||||
|
|
||||||
_onNewMailReceived: (incoming) ->
|
|
||||||
new Promise (resolve, reject) =>
|
|
||||||
return resolve() if NylasEnv.config.get('core.notifications.enabled') is false
|
|
||||||
|
|
||||||
incomingMessages = incoming['message'] ? []
|
|
||||||
incomingThreads = incoming['thread'] ? []
|
|
||||||
|
|
||||||
# Filter for new messages that are not sent by the current user
|
|
||||||
newUnread = _.filter incomingMessages, (msg) =>
|
|
||||||
isUnread = msg.unread is true
|
|
||||||
isNew = msg.date?.valueOf() >= @activationTime
|
|
||||||
isFromMe = msg.isFromMe()
|
|
||||||
return isUnread and isNew and not isFromMe
|
|
||||||
|
|
||||||
return resolve() if newUnread.length is 0
|
|
||||||
|
|
||||||
# For each message, find it's corresponding thread. First, look to see
|
|
||||||
# if it's already in the `incoming` payload (sent via delta sync
|
|
||||||
# at the same time as the message.) If it's not, try loading it from
|
|
||||||
# the local cache.
|
|
||||||
#
|
|
||||||
# Note we may receive multiple unread msgs for the same thread.
|
|
||||||
# Using a map and ?= to avoid repeating work.
|
|
||||||
threads = {}
|
|
||||||
for msg in newUnread
|
|
||||||
threads[msg.threadId] ?= _.findWhere(incomingThreads, {id: msg.threadId})
|
|
||||||
threads[msg.threadId] ?= DatabaseStore.find(Thread, msg.threadId)
|
|
||||||
|
|
||||||
Promise.props(threads).then (threads) =>
|
|
||||||
# Filter new unread messages to just the ones in the inbox
|
|
||||||
newUnreadInInbox = _.filter newUnread, (msg) ->
|
|
||||||
threads[msg.threadId]?.categoryNamed('inbox')
|
|
||||||
|
|
||||||
# Filter messages that we can't decide whether to display or not
|
|
||||||
# since no associated Thread object has arrived yet.
|
|
||||||
newUnreadMissingThreads = _.filter newUnread, (msg) ->
|
|
||||||
not threads[msg.threadId]?
|
|
||||||
|
|
||||||
if newUnreadMissingThreads.length > 0
|
|
||||||
@_onNewMessagesMissingThreads(newUnreadMissingThreads)
|
|
||||||
|
|
||||||
return resolve() if newUnreadInInbox.length is 0
|
|
||||||
if NylasEnv.config.get("core.notifications.sounds")
|
|
||||||
SoundRegistry.playSound('new-mail')
|
|
||||||
|
|
||||||
for msg in newUnreadInInbox
|
|
||||||
@stack.push({message: msg, thread: threads[msg.threadId]})
|
|
||||||
if not @stackProcessTimer
|
|
||||||
@_notifyMessages()
|
|
||||||
|
|
||||||
resolve()
|
|
189
internal_packages/unread-notifications/lib/main.es6
Normal file
189
internal_packages/unread-notifications/lib/main.es6
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import _ from 'underscore'
|
||||||
|
import {
|
||||||
|
Thread,
|
||||||
|
Actions,
|
||||||
|
MailboxPerspective,
|
||||||
|
SoundRegistry,
|
||||||
|
FocusedPerspectiveStore,
|
||||||
|
NativeNotifications,
|
||||||
|
DatabaseStore,
|
||||||
|
} from 'nylas-exports';
|
||||||
|
|
||||||
|
export class Notifier {
|
||||||
|
constructor() {
|
||||||
|
this.unlisteners = [];
|
||||||
|
this.unlisteners.push(Actions.didPassivelyReceiveNewModels.listen(this._onNewMailReceived, this));
|
||||||
|
this.activationTime = Date.now();
|
||||||
|
this.stack = [];
|
||||||
|
this.stackProcessTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten() {
|
||||||
|
for (const unlisten of this.unlisteners) {
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyAll() {
|
||||||
|
NativeNotifications.displayNotification({
|
||||||
|
title: `${this.stack.length} Unread Messages`,
|
||||||
|
tag: 'unread-update',
|
||||||
|
});
|
||||||
|
this.stack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyOne({message, thread}) {
|
||||||
|
const from = (message.from[0]) ? message.from[0].displayName() : "Unknown";
|
||||||
|
const title = from;
|
||||||
|
let subtitle = null;
|
||||||
|
let body = null;
|
||||||
|
if (message.subject && message.subject.length > 0) {
|
||||||
|
subtitle = message.subject;
|
||||||
|
body = message.snippet;
|
||||||
|
} else {
|
||||||
|
subtitle = message.snippet
|
||||||
|
body = null
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeNotifications.displayNotification({
|
||||||
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
body: body,
|
||||||
|
canReply: true,
|
||||||
|
tag: 'unread-update',
|
||||||
|
onActivate: ({response, activationType}) => {
|
||||||
|
if ((activationType === 'replied') && response && _.isString(response)) {
|
||||||
|
Actions.sendQuickReply({thread, message}, response);
|
||||||
|
} else {
|
||||||
|
NylasEnv.displayWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCategories = FocusedPerspectiveStore.current().categories();
|
||||||
|
const desiredCategory = thread.categoryNamed('inbox');
|
||||||
|
|
||||||
|
if (!desiredCategory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_.pluck(currentCategories, 'id').includes(desiredCategory.id)) {
|
||||||
|
const filter = MailboxPerspective.forCategory(desiredCategory);
|
||||||
|
Actions.focusMailboxPerspective(filter);
|
||||||
|
}
|
||||||
|
Actions.setFocus({collection: 'thread', item: thread});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyMessages() {
|
||||||
|
if (this.stack.length >= 5) {
|
||||||
|
this._notifyAll()
|
||||||
|
} else if (this.stack.length > 0) {
|
||||||
|
this._notifyOne(this.stack.pop());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stackProcessTimer = null;
|
||||||
|
if (this.stack.length > 0) {
|
||||||
|
this.stackProcessTimer = setTimeout(() => this._notifyMessages(), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://phab.nylas.com/D2188
|
||||||
|
_onNewMessagesMissingThreads(messages) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const threads = {}
|
||||||
|
for (const {threadId} of messages) {
|
||||||
|
threads[threadId] = threads[threadId] || DatabaseStore.find(Thread, threadId);
|
||||||
|
}
|
||||||
|
Promise.props(threads).then((resolvedThreads) => {
|
||||||
|
const resolved = messages.filter((msg) => resolvedThreads[msg.threadId]);
|
||||||
|
if (resolved.length > 0) {
|
||||||
|
this._onNewMailReceived({message: resolved, thread: _.values(resolvedThreads)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onNewMailReceived(incoming) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (NylasEnv.config.get('core.notifications.enabled') === false) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomingMessages = incoming.message || [];
|
||||||
|
const incomingThreads = incoming.thread || [];
|
||||||
|
|
||||||
|
// Filter for new messages that are not sent by the current user
|
||||||
|
const newUnread = incomingMessages.filter((msg) => {
|
||||||
|
const isUnread = msg.unread === true;
|
||||||
|
const isNew = msg.date && msg.date.valueOf() >= this.activationTime;
|
||||||
|
const isFromMe = msg.isFromMe();
|
||||||
|
return isUnread && isNew && !isFromMe;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newUnread.length === 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each message, find it's corresponding thread. First, look to see
|
||||||
|
// if it's already in the `incoming` payload (sent via delta sync
|
||||||
|
// at the same time as the message.) If it's not, try loading it from
|
||||||
|
// the local cache.
|
||||||
|
|
||||||
|
// Note we may receive multiple unread msgs for the same thread.
|
||||||
|
// Using a map and ?= to avoid repeating work.
|
||||||
|
const threads = {}
|
||||||
|
for (const {threadId} of newUnread) {
|
||||||
|
threads[threadId] = threads[threadId] || _.findWhere(incomingThreads, {id: threadId})
|
||||||
|
threads[threadId] = threads[threadId] || DatabaseStore.find(Thread, threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.props(threads).then((resolvedThreads) => {
|
||||||
|
// Filter new unread messages to just the ones in the inbox
|
||||||
|
const newUnreadInInbox = newUnread.filter((msg) =>
|
||||||
|
resolvedThreads[msg.threadId] && resolvedThreads[msg.threadId].categoryNamed('inbox')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter messages that we can't decide whether to display or not
|
||||||
|
// since no associated Thread object has arrived yet.
|
||||||
|
const newUnreadMissingThreads = newUnread.filter((msg) => !resolvedThreads[msg.threadId])
|
||||||
|
|
||||||
|
if (newUnreadMissingThreads.length > 0) {
|
||||||
|
this._onNewMessagesMissingThreads(newUnreadMissingThreads);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUnreadInInbox.length === 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of newUnreadInInbox) {
|
||||||
|
this.stack.push({message: msg, thread: resolvedThreads[msg.threadId]});
|
||||||
|
}
|
||||||
|
if (!this.stackProcessTimer) {
|
||||||
|
if (NylasEnv.config.get("core.notifications.sounds")) {
|
||||||
|
SoundRegistry.playSound('new-mail');
|
||||||
|
}
|
||||||
|
this._notifyMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
enabled: {
|
||||||
|
'type': 'boolean',
|
||||||
|
'default': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function activate() {
|
||||||
|
this.notifier = new Notifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate() {
|
||||||
|
this.notifier.unlisten();
|
||||||
|
}
|
|
@ -1,277 +0,0 @@
|
||||||
_ = require 'underscore'
|
|
||||||
Contact = require '../../../src/flux/models/contact'
|
|
||||||
Message = require('../../../src/flux/models/message').default
|
|
||||||
Thread = require('../../../src/flux/models/thread').default
|
|
||||||
Category = require '../../../src/flux/models/category'
|
|
||||||
CategoryStore = require '../../../src/flux/stores/category-store'
|
|
||||||
DatabaseStore = require '../../../src/flux/stores/database-store'
|
|
||||||
AccountStore = require '../../../src/flux/stores/account-store'
|
|
||||||
SoundRegistry = require '../../../src/sound-registry'
|
|
||||||
NativeNotifications = require '../../../src/native-notifications'
|
|
||||||
Main = require '../lib/main'
|
|
||||||
|
|
||||||
describe "UnreadNotifications", ->
|
|
||||||
beforeEach ->
|
|
||||||
Main.activate()
|
|
||||||
|
|
||||||
inbox = new Category(id: "l1", name: "inbox", displayName: "Inbox")
|
|
||||||
archive = new Category(id: "l2", name: "archive", displayName: "Archive")
|
|
||||||
|
|
||||||
spyOn(CategoryStore, "getStandardCategory").andReturn inbox
|
|
||||||
|
|
||||||
account = AccountStore.accounts()[0]
|
|
||||||
|
|
||||||
@threadA = new Thread
|
|
||||||
categories: [inbox]
|
|
||||||
@threadB = new Thread
|
|
||||||
categories: [archive]
|
|
||||||
|
|
||||||
@msg1 = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "A"
|
|
||||||
@msgNoSender = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: []
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "A"
|
|
||||||
@msg2 = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
|
|
||||||
subject: "Hello World 2"
|
|
||||||
threadId: "A"
|
|
||||||
@msg3 = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "A"
|
|
||||||
@msg4 = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "A"
|
|
||||||
@msg5 = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "A"
|
|
||||||
@msgUnreadButArchived = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
|
|
||||||
subject: "Hello World 2"
|
|
||||||
threadId: "B"
|
|
||||||
@msgRead = new Message
|
|
||||||
unread: false
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
|
|
||||||
subject: "Hello World Read Already"
|
|
||||||
threadId: "A"
|
|
||||||
@msgOld = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date(2000,1,1)
|
|
||||||
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
|
|
||||||
subject: "Hello World Old"
|
|
||||||
threadId: "A"
|
|
||||||
@msgFromMe = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [account.me()]
|
|
||||||
subject: "A Sent Mail!"
|
|
||||||
threadId: "A"
|
|
||||||
|
|
||||||
spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
|
|
||||||
return Promise.resolve(@threadA) if id is 'A'
|
|
||||||
return Promise.resolve(@threadB) if id is 'B'
|
|
||||||
return Promise.resolve(null)
|
|
||||||
|
|
||||||
spyOn(NativeNotifications, 'displayNotification').andCallFake ->
|
|
||||||
spyOn(Promise, 'props').andCallFake (dict) ->
|
|
||||||
dictOut = {}
|
|
||||||
for key, val of dict
|
|
||||||
if val.value?
|
|
||||||
dictOut[key] = val.value()
|
|
||||||
else
|
|
||||||
dictOut[key] = val
|
|
||||||
Promise.resolve(dictOut)
|
|
||||||
|
|
||||||
afterEach ->
|
|
||||||
Main.deactivate()
|
|
||||||
|
|
||||||
it "should create a Notification if there is one unread message", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgRead, @msg1]})
|
|
||||||
.then ->
|
|
||||||
advanceClock(2000)
|
|
||||||
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
|
||||||
options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
|
||||||
delete options['onActivate']
|
|
||||||
expect(options).toEqual({
|
|
||||||
title: 'Ben',
|
|
||||||
subtitle: 'Hello World',
|
|
||||||
body: undefined,
|
|
||||||
canReply: true,
|
|
||||||
tag: 'unread-update'
|
|
||||||
})
|
|
||||||
|
|
||||||
it "should create multiple Notifications if there is more than one but less than five unread messages", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msg1, @msg2, @msg3]})
|
|
||||||
.then ->
|
|
||||||
#Need to call advance clock twice because we call setTimeout twice
|
|
||||||
advanceClock(2000)
|
|
||||||
advanceClock(2000)
|
|
||||||
expect(NativeNotifications.displayNotification.callCount).toEqual(3)
|
|
||||||
|
|
||||||
it "should create a Notification if there are five or more unread messages", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({
|
|
||||||
message: [@msg1, @msg2, @msg3, @msg4, @msg5]})
|
|
||||||
.then ->
|
|
||||||
advanceClock(2000)
|
|
||||||
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
|
||||||
expect(NativeNotifications.displayNotification.mostRecentCall.args).toEqual([{
|
|
||||||
title: '5 Unread Messages',
|
|
||||||
tag: 'unread-update'
|
|
||||||
}])
|
|
||||||
|
|
||||||
it "should create a Notification correctly, even if new mail has no sender", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgNoSender]})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
|
||||||
|
|
||||||
options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
|
||||||
delete options['onActivate']
|
|
||||||
expect(options).toEqual({
|
|
||||||
title: 'Unknown',
|
|
||||||
subtitle: 'Hello World',
|
|
||||||
body: undefined,
|
|
||||||
canReply : true,
|
|
||||||
tag: 'unread-update'
|
|
||||||
})
|
|
||||||
|
|
||||||
it "should not create a Notification if there are no new messages", ->
|
|
||||||
waitsForPromise ->
|
|
||||||
Main._onNewMailReceived({message: []})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
waitsForPromise ->
|
|
||||||
Main._onNewMailReceived({})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should not notify about unread messages that are outside the inbox", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgUnreadButArchived, @msg1]})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
|
||||||
options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
|
||||||
delete options['onActivate']
|
|
||||||
expect(options).toEqual({
|
|
||||||
title: 'Ben',
|
|
||||||
subtitle: 'Hello World',
|
|
||||||
body: undefined,
|
|
||||||
canReply : true,
|
|
||||||
tag: 'unread-update'
|
|
||||||
})
|
|
||||||
|
|
||||||
it "should not create a Notification if the new messages are not unread", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgRead]})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should not create a Notification if the new messages are actually old ones", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgOld]})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should not create a Notification if the new message is one I sent", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgFromMe]})
|
|
||||||
.then ->
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should play a sound when it gets new mail", ->
|
|
||||||
spyOn(NylasEnv.config, "get").andCallFake (config) ->
|
|
||||||
if config is "core.notifications.enabled" then return true
|
|
||||||
if config is "core.notifications.sounds" then return true
|
|
||||||
|
|
||||||
spyOn(SoundRegistry, "playSound")
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msg1]})
|
|
||||||
.then ->
|
|
||||||
expect(NylasEnv.config.get.calls[1].args[0]).toBe "core.notifications.sounds"
|
|
||||||
expect(SoundRegistry.playSound).toHaveBeenCalledWith("new-mail")
|
|
||||||
|
|
||||||
it "should not play a sound if the config is off", ->
|
|
||||||
spyOn(NylasEnv.config, "get").andCallFake (config) ->
|
|
||||||
if config is "core.notifications.enabled" then return true
|
|
||||||
if config is "core.notifications.sounds" then return false
|
|
||||||
spyOn(SoundRegistry, "playSound")
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msg1]})
|
|
||||||
.then ->
|
|
||||||
expect(NylasEnv.config.get.calls[1].args[0]).toBe "core.notifications.sounds"
|
|
||||||
expect(SoundRegistry.playSound).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
describe "when the message has no matching thread", ->
|
|
||||||
beforeEach ->
|
|
||||||
@msgNoThread = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "missing"
|
|
||||||
|
|
||||||
it "should not create a Notification, since it cannot be determined whether the message is in the Inbox", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
Main._onNewMailReceived({message: [@msgNoThread]})
|
|
||||||
.then ->
|
|
||||||
advanceClock(2000)
|
|
||||||
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
it "should call _onNewMessagesMissingThreads to try displaying a notification again in 10 seconds", ->
|
|
||||||
waitsForPromise =>
|
|
||||||
spyOn(Main, '_onNewMessagesMissingThreads')
|
|
||||||
Main._onNewMailReceived({message: [@msgNoThread]})
|
|
||||||
.then =>
|
|
||||||
advanceClock(2000)
|
|
||||||
expect(Main._onNewMessagesMissingThreads).toHaveBeenCalledWith([@msgNoThread])
|
|
||||||
|
|
||||||
describe "_onNewMessagesMissingThreads", ->
|
|
||||||
beforeEach ->
|
|
||||||
@msgNoThread = new Message
|
|
||||||
unread: true
|
|
||||||
date: new Date()
|
|
||||||
from: [new Contact(name: 'Ben', email: 'ben@example.com')]
|
|
||||||
subject: "Hello World"
|
|
||||||
threadId: "missing"
|
|
||||||
spyOn(Main, '_onNewMailReceived')
|
|
||||||
Main._onNewMessagesMissingThreads([@msgNoThread])
|
|
||||||
advanceClock(2000)
|
|
||||||
|
|
||||||
it "should wait 10 seconds and then re-query for threads", ->
|
|
||||||
expect(DatabaseStore.find).not.toHaveBeenCalled()
|
|
||||||
@msgNoThread.threadId = "A"
|
|
||||||
advanceClock(10000)
|
|
||||||
expect(DatabaseStore.find).toHaveBeenCalled()
|
|
||||||
advanceClock()
|
|
||||||
expect(Main._onNewMailReceived).toHaveBeenCalledWith({message: [@msgNoThread], thread: [@threadA]})
|
|
||||||
|
|
||||||
it "should do nothing if the threads still can't be found", ->
|
|
||||||
expect(DatabaseStore.find).not.toHaveBeenCalled()
|
|
||||||
advanceClock(10000)
|
|
||||||
expect(DatabaseStore.find).toHaveBeenCalled()
|
|
||||||
advanceClock()
|
|
||||||
expect(Main._onNewMailReceived).not.toHaveBeenCalled()
|
|
371
internal_packages/unread-notifications/spec/main-spec.es6
Normal file
371
internal_packages/unread-notifications/spec/main-spec.es6
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
import Contact from '../../../src/flux/models/contact'
|
||||||
|
import Message from '../../../src/flux/models/message'
|
||||||
|
import Thread from '../../../src/flux/models/thread'
|
||||||
|
import Category from '../../../src/flux/models/category'
|
||||||
|
import CategoryStore from '../../../src/flux/stores/category-store'
|
||||||
|
import DatabaseStore from '../../../src/flux/stores/database-store'
|
||||||
|
import AccountStore from '../../../src/flux/stores/account-store'
|
||||||
|
import SoundRegistry from '../../../src/sound-registry'
|
||||||
|
import NativeNotifications from '../../../src/native-notifications'
|
||||||
|
import {Notifier} from '../lib/main'
|
||||||
|
|
||||||
|
describe("UnreadNotifications", function UnreadNotifications() {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.notifier = new Notifier();
|
||||||
|
|
||||||
|
const inbox = new Category({id: "l1", name: "inbox", displayName: "Inbox"})
|
||||||
|
const archive = new Category({id: "l2", name: "archive", displayName: "Archive"})
|
||||||
|
|
||||||
|
spyOn(CategoryStore, "getStandardCategory").andReturn(inbox);
|
||||||
|
|
||||||
|
const account = AccountStore.accounts()[0];
|
||||||
|
|
||||||
|
this.threadA = new Thread({
|
||||||
|
categories: [inbox],
|
||||||
|
});
|
||||||
|
this.threadB = new Thread({
|
||||||
|
categories: [archive],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.msg1 = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msgNoSender = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msg2 = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],
|
||||||
|
subject: "Hello World 2",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msg3 = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msg4 = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msg5 = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msgUnreadButArchived = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],
|
||||||
|
subject: "Hello World 2",
|
||||||
|
threadId: "B",
|
||||||
|
});
|
||||||
|
this.msgRead = new Message({
|
||||||
|
unread: false,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],
|
||||||
|
subject: "Hello World Read Already",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msgOld = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(2000, 1, 1),
|
||||||
|
from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],
|
||||||
|
subject: "Hello World Old",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
this.msgFromMe = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [account.me()],
|
||||||
|
subject: "A Sent Mail!",
|
||||||
|
threadId: "A",
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(DatabaseStore, 'find').andCallFake((klass, id) => {
|
||||||
|
if (id === 'A') {
|
||||||
|
return Promise.resolve(this.threadA);
|
||||||
|
}
|
||||||
|
if (id === 'B') {
|
||||||
|
return Promise.resolve(this.threadB);
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(NativeNotifications, 'displayNotification').andCallFake(() => false)
|
||||||
|
spyOn(Promise, 'props').andCallFake((dict) => {
|
||||||
|
const dictOut = {};
|
||||||
|
for (const key of Object.keys(dict)) {
|
||||||
|
const val = dict[key];
|
||||||
|
if (val.value !== undefined) {
|
||||||
|
dictOut[key] = val.value();
|
||||||
|
} else {
|
||||||
|
dictOut[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(dictOut);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
this.notifier.unlisten();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a Notification if there is one unread message", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgRead, this.msg1]})
|
||||||
|
.then(() => {
|
||||||
|
advanceClock(2000)
|
||||||
|
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
||||||
|
const options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
||||||
|
delete options.onActivate;
|
||||||
|
expect(options).toEqual({
|
||||||
|
title: 'Ben',
|
||||||
|
subtitle: 'Hello World',
|
||||||
|
body: undefined,
|
||||||
|
canReply: true,
|
||||||
|
tag: 'unread-update',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create multiple Notifications if there is more than one but less than five unread messages", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msg1, this.msg2, this.msg3]})
|
||||||
|
.then(() => {
|
||||||
|
// Need to call advance clock twice because we call setTimeout twice
|
||||||
|
advanceClock(2000)
|
||||||
|
advanceClock(2000)
|
||||||
|
expect(NativeNotifications.displayNotification.callCount).toEqual(3)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a Notification if there are five or more unread messages", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({
|
||||||
|
message: [this.msg1, this.msg2, this.msg3, this.msg4, this.msg5]})
|
||||||
|
.then(() => {
|
||||||
|
advanceClock(2000)
|
||||||
|
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
||||||
|
expect(NativeNotifications.displayNotification.mostRecentCall.args).toEqual([{
|
||||||
|
title: '5 Unread Messages',
|
||||||
|
tag: 'unread-update',
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a Notification correctly, even if new mail has no sender", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgNoSender]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
||||||
|
delete options.onActivate;
|
||||||
|
expect(options).toEqual({
|
||||||
|
title: 'Unknown',
|
||||||
|
subtitle: 'Hello World',
|
||||||
|
body: undefined,
|
||||||
|
canReply: true,
|
||||||
|
tag: 'unread-update',
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a Notification if there are no new messages", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: []})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not notify about unread messages that are outside the inbox", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgUnreadButArchived, this.msg1]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).toHaveBeenCalled()
|
||||||
|
const options = NativeNotifications.displayNotification.mostRecentCall.args[0]
|
||||||
|
delete options.onActivate;
|
||||||
|
expect(options).toEqual({
|
||||||
|
title: 'Ben',
|
||||||
|
subtitle: 'Hello World',
|
||||||
|
body: undefined,
|
||||||
|
canReply: true,
|
||||||
|
tag: 'unread-update',
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a Notification if the new messages are not unread", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgRead]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a Notification if the new messages are actually old ones", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgOld]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a Notification if the new message is one I sent", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgFromMe]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should play a sound when it gets new mail", () => {
|
||||||
|
spyOn(NylasEnv.config, "get").andCallFake((config) => {
|
||||||
|
if (config === "core.notifications.enabled") return true
|
||||||
|
if (config === "core.notifications.sounds") return true
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(SoundRegistry, "playSound");
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msg1]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NylasEnv.config.get.calls[1].args[0]).toBe("core.notifications.sounds");
|
||||||
|
expect(SoundRegistry.playSound).toHaveBeenCalledWith("new-mail");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not play a sound if the config is off", () => {
|
||||||
|
spyOn(NylasEnv.config, "get").andCallFake((config) => {
|
||||||
|
if (config === "core.notifications.enabled") return true;
|
||||||
|
if (config === "core.notifications.sounds") return false;
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
spyOn(SoundRegistry, "playSound")
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msg1]})
|
||||||
|
.then(() => {
|
||||||
|
expect(NylasEnv.config.get.calls[1].args[0]).toBe("core.notifications.sounds");
|
||||||
|
expect(SoundRegistry.playSound).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not play a sound if other notiications are still in flight", () => {
|
||||||
|
spyOn(NylasEnv.config, "get").andCallFake((config) => {
|
||||||
|
if (config === "core.notifications.enabled") return true;
|
||||||
|
if (config === "core.notifications.sounds") return true;
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
waitsForPromise(() => {
|
||||||
|
spyOn(SoundRegistry, "playSound")
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msg1, this.msg2]}).then(() => {
|
||||||
|
expect(SoundRegistry.playSound).toHaveBeenCalled();
|
||||||
|
SoundRegistry.playSound.reset();
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msg3]}).then(() => {
|
||||||
|
expect(SoundRegistry.playSound).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the message has no matching thread", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.msgNoThread = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "missing",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a Notification, since it cannot be determined whether the message is in the Inbox", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgNoThread]})
|
||||||
|
.then(() => {
|
||||||
|
advanceClock(2000)
|
||||||
|
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call _onNewMessagesMissingThreads to try displaying a notification again in 10 seconds", () => {
|
||||||
|
waitsForPromise(() => {
|
||||||
|
spyOn(this.notifier, '_onNewMessagesMissingThreads')
|
||||||
|
return this.notifier._onNewMailReceived({message: [this.msgNoThread]})
|
||||||
|
.then(() => {
|
||||||
|
advanceClock(2000)
|
||||||
|
expect(this.notifier._onNewMessagesMissingThreads).toHaveBeenCalledWith([this.msgNoThread])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("_onNewMessagesMissingThreads", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.msgNoThread = new Message({
|
||||||
|
unread: true,
|
||||||
|
date: new Date(),
|
||||||
|
from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],
|
||||||
|
subject: "Hello World",
|
||||||
|
threadId: "missing",
|
||||||
|
});
|
||||||
|
spyOn(this.notifier, '_onNewMailReceived')
|
||||||
|
this.notifier._onNewMessagesMissingThreads([this.msgNoThread])
|
||||||
|
advanceClock(2000)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wait 10 seconds and then re-query for threads", () => {
|
||||||
|
expect(DatabaseStore.find).not.toHaveBeenCalled()
|
||||||
|
this.msgNoThread.threadId = "A"
|
||||||
|
advanceClock(10000)
|
||||||
|
expect(DatabaseStore.find).toHaveBeenCalled()
|
||||||
|
advanceClock()
|
||||||
|
expect(this.notifier._onNewMailReceived).toHaveBeenCalledWith({message: [this.msgNoThread], thread: [this.threadA]})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should do nothing if the threads still can't be found", () => {
|
||||||
|
expect(DatabaseStore.find).not.toHaveBeenCalled()
|
||||||
|
advanceClock(10000)
|
||||||
|
expect(DatabaseStore.find).toHaveBeenCalled()
|
||||||
|
advanceClock()
|
||||||
|
expect(this.notifier._onNewMailReceived).not.toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue