Ensure v1 models reach the client, fix notification issues #160, #154

This commit is contained in:
Ben Gotow 2017-10-19 23:41:20 -07:00
parent 78c95c2694
commit a84b63d1f5
3 changed files with 121 additions and 29 deletions

View file

@ -2,12 +2,15 @@ import _ from 'underscore';
import { import {
Thread, Thread,
Actions, Actions,
AccountStore,
Message, Message,
SoundRegistry, SoundRegistry,
NativeNotifications, NativeNotifications,
DatabaseStore, DatabaseStore,
} from 'mailspring-exports'; } from 'mailspring-exports';
const WAIT_FOR_CHANGES_DELAY = 400;
export class Notifier { export class Notifier {
constructor() { constructor() {
this.activationTime = Date.now(); this.activationTime = Date.now();
@ -26,38 +29,79 @@ export class Notifier {
// async for testing // async for testing
async _onDatabaseChanged({ objectClass, objects }) { async _onDatabaseChanged({ objectClass, objects }) {
if (AppEnv.config.get('core.notifications.enabled') === false) {
return;
}
if (objectClass === Thread.name) { if (objectClass === Thread.name) {
// Ensure notifications are dismissed when the user reads a thread return this._onThreadsChanged(objects);
objects.forEach(({ id, unread }) => {
if (!unread && this.activeNotifications[id]) {
this.activeNotifications[id].forEach(n => n.close());
delete this.activeNotifications[id];
}
});
} }
if (objectClass === Message.name) { if (objectClass === Message.name) {
if (AppEnv.config.get('core.notifications.enabled') === false) { return this._onMessagesChanged(objects);
return;
}
const newUnread = objects.filter(msg => {
// ensure the message is unread
if (msg.unread !== true) return false;
// ensure the message was just saved (eg: this is not a modification)
if (msg.version !== 1) return false;
// ensure the message was received after the app launched (eg: not syncing an old email)
if (!msg.date || msg.date.valueOf() < this.activationTime) return false;
// ensure the message is from someone else
if (msg.isFromMe()) return false;
return true;
});
if (newUnread.length > 0) {
await this._onNewMessagesReceived(newUnread);
}
} }
} }
// async for testing
async _onMessagesChanged(msgs) {
const notifworthy = {};
for (const msg of msgs) {
// ensure the message is unread
if (msg.unread !== true) continue;
// ensure the message was just created (eg: this is not a modification)
if (msg.version !== 1) continue;
// ensure the message was received after the app launched (eg: not syncing an old email)
if (!msg.date || msg.date.valueOf() < this.activationTime) continue;
// ensure the message is not a loopback
const account = msg.from[0] && AccountStore.accountForEmail(msg.from[0].email);
if (msg.accountId === (account || {}).id) continue;
notifworthy[msg.id] = msg;
}
if (Object.keys(notifworthy).length === 0) {
return;
}
if (!AppEnv.inSpecMode()) {
await new Promise(resolve => {
// wait a couple hundred milliseconds and collect any updates to these
// new messages. This gets us message bodies, messages impacted by mail rules, etc.
// while ensuring notifications are never too delayed.
const unlisten = DatabaseStore.listen(({ objectClass, objects }) => {
if (objectClass !== Message.name) {
return;
}
for (const msg of objects) {
if (notifworthy[msg.id]) {
notifworthy[msg.id] = msg;
if (msg.unread === false) {
delete notifworthy[msg.id];
}
}
}
});
setTimeout(() => {
unlisten();
resolve();
}, WAIT_FOR_CHANGES_DELAY);
});
}
await this._onNewMessagesReceived(Object.values(notifworthy));
}
_onThreadsChanged(threads) {
// Ensure notifications are dismissed when the user reads a thread
threads.forEach(({ id, unread }) => {
if (!unread && this.activeNotifications[id]) {
this.activeNotifications[id].forEach(n => n.close());
delete this.activeNotifications[id];
}
});
}
_notifyAll() { _notifyAll() {
NativeNotifications.displayNotification({ NativeNotifications.displayNotification({
title: `${this.unnotifiedQueue.length} Unread Messages`, title: `${this.unnotifiedQueue.length} Unread Messages`,
@ -123,6 +167,10 @@ export class Notifier {
} }
_onNewMessagesReceived(newMessages) { _onNewMessagesReceived(newMessages) {
if (newMessages.length === 0) {
return Promise.resolve();
}
// For each message, find it's corresponding thread. First, look to see // For each message, find it's corresponding thread. First, look to see
// if it's already in the `incoming` payload (sent via delta sync // 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 // at the same time as the message.) If it's not, try loading it from

View file

@ -25,96 +25,130 @@ describe('UnreadNotifications', function UnreadNotifications() {
this.threadA = new Thread({ this.threadA = new Thread({
id: 'A', id: 'A',
accountId: 'a',
folders: [inbox], folders: [inbox],
}); });
this.threadB = new Thread({ this.threadB = new Thread({
id: 'B', id: 'B',
accountId: 'a',
folders: [archive], folders: [archive],
}); });
this.msg1 = new Message({ this.msg1 = new Message({
id: '1',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })], from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })],
subject: 'Hello World', subject: 'Hello World',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msgNoSender = new Message({ this.msgNoSender = new Message({
id: 'no',
unread: true, unread: true,
date: new Date(), date: new Date(),
from: [], from: [],
accountId: 'a',
subject: 'Hello World', subject: 'Hello World',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msg2 = new Message({ this.msg2 = new Message({
id: '2',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })], from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })],
subject: 'Hello World 2', subject: 'Hello World 2',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msg3 = new Message({ this.msg3 = new Message({
id: '3',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })], from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })],
subject: 'Hello World 3', subject: 'Hello World 3',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msg4 = new Message({ this.msg4 = new Message({
id: '4',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })], from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })],
subject: 'Hello World 4', subject: 'Hello World 4',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msg5 = new Message({ this.msg5 = new Message({
id: '5',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })], from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })],
subject: 'Hello World 5', subject: 'Hello World 5',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msgUnreadButArchived = new Message({ this.msgUnreadButArchived = new Message({
id: 'uba',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })], from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })],
subject: 'Hello World 2', subject: 'Hello World 2',
threadId: 'B', threadId: 'B',
version: 1, version: 1,
}); });
this.msgRead = new Message({ this.msgRead = new Message({
id: 'read',
unread: false, unread: false,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })], from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })],
subject: 'Hello World Read Already', subject: 'Hello World Read Already',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msgOld = new Message({ this.msgOld = new Message({
id: 'old',
unread: true, unread: true,
date: new Date(2000, 1, 1), date: new Date(2000, 1, 1),
accountId: 'a',
from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })], from: [new Contact({ name: 'Mark', email: 'markthis.example.com' })],
subject: 'Hello World Old', subject: 'Hello World Old',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msgFromMe = new Message({ this.msgFromMeSameAccount = new Message({
id: 'from-me',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: account.id,
from: [account.me()],
subject: 'A Sent Mail!',
threadId: 'A',
version: 1,
});
this.msgFromMeDiffAccount = new Message({
id: 'from-me-diff',
unread: true,
date: new Date(),
accountId: 'other',
from: [account.me()], from: [account.me()],
subject: 'A Sent Mail!', subject: 'A Sent Mail!',
threadId: 'A', threadId: 'A',
version: 1, version: 1,
}); });
this.msgHigherVersion = new Message({ this.msgHigherVersion = new Message({
id: 'hv',
unread: true, unread: true,
date: new Date(), date: new Date(),
accountId: 'a',
from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })], from: [new Contact({ name: 'Ben', email: 'benthis.example.com' })],
subject: 'Hello World', subject: 'Hello World',
threadId: 'A', threadId: 'A',
@ -308,16 +342,26 @@ describe('UnreadNotifications', function UnreadNotifications() {
}); });
}); });
it('should not create a Notification if the new message is one I sent', () => { it('should not create a Notification if the new message is one I sent from the same account', () => {
waitsForPromise(async () => { waitsForPromise(async () => {
await this.notifier._onDatabaseChanged({ await this.notifier._onDatabaseChanged({
objectClass: Message.name, objectClass: Message.name,
objects: [this.msgFromMe], objects: [this.msgFromMeSameAccount],
}); });
expect(NativeNotifications.displayNotification).not.toHaveBeenCalled(); expect(NativeNotifications.displayNotification).not.toHaveBeenCalled();
}); });
}); });
it('should xcreate a Notification if the new message is one I sent from a different linked account', () => {
waitsForPromise(async () => {
await this.notifier._onDatabaseChanged({
objectClass: Message.name,
objects: [this.msgFromMeDiffAccount],
});
expect(NativeNotifications.displayNotification).toHaveBeenCalled();
});
});
it('clears notifications when a thread is read', () => { it('clears notifications when a thread is read', () => {
waitsForPromise(async () => { waitsForPromise(async () => {
await this.notifier._onDatabaseChanged({ await this.notifier._onDatabaseChanged({

@ -1 +1 @@
Subproject commit dd26a2758c38019b39ca30a68cd36cde721a918a Subproject commit 8841ba0989bcc6179a88f166a24a0634e22a41c6