mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-24 08:04:11 +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