diff --git a/background.html b/background.html
index 47c2897bd..5004ac77f 100644
--- a/background.html
+++ b/background.html
@@ -35,11 +35,6 @@
-
-
-
-
-
diff --git a/js/background.js b/js/background.js
index c55d0c988..548649428 100644
--- a/js/background.js
+++ b/js/background.js
@@ -319,7 +319,7 @@
window.setCallMediaPermissions(enabled);
};
- Whisper.Notifications.on('click', async conversationKey => {
+ window.openFromNotification = async conversationKey => {
window.showWindow();
if (conversationKey) {
// do not put the messageId here so the conversation is loaded on the last unread instead
@@ -331,6 +331,7 @@
}
});
+
Whisper.events.on('openInbox', () => {
appView.openInbox({
initialLoadComplete,
diff --git a/js/modules/signal.js b/js/modules/signal.js
index 2b87017bb..b0326a7dd 100644
--- a/js/modules/signal.js
+++ b/js/modules/signal.js
@@ -2,7 +2,6 @@
const Crypto = require('./crypto');
const Data = require('../../ts/data/data');
-const Notifications = require('../../ts/notifications');
const OS = require('../../ts/OS');
const Settings = require('./settings');
const Util = require('../../ts/util');
@@ -37,7 +36,6 @@ exports.setup = () => {
Crypto,
Data,
LinkPreviews,
- Notifications,
OS,
Settings,
Types,
diff --git a/js/notifications.js b/js/notifications.js
deleted file mode 100644
index b7639bbfd..000000000
--- a/js/notifications.js
+++ /dev/null
@@ -1,194 +0,0 @@
-/* global Signal:false */
-/* global Backbone: false */
-
-/* global drawAttention: false */
-/* global i18n: false */
-/* global isFocused: false */
-/* global Signal: false */
-/* global storage: false */
-/* global Whisper: false */
-/* global _: false */
-
-// eslint-disable-next-line func-names
-(function() {
- 'use strict';
-
- window.Whisper = window.Whisper || {};
- const { Settings } = Signal.Types;
-
- const SettingNames = {
- COUNT: 'count',
- NAME: 'name',
- MESSAGE: 'message',
- };
-
- function filter(text) {
- return (text || '')
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(/'/g, ''')
- .replace(//g, '>');
- }
-
- Whisper.Notifications = new (Backbone.Collection.extend({
- initialize() {
- this.isEnabled = false;
-
- this.lastNotification = null;
-
- // Testing indicated that trying to create/destroy notifications too quickly
- // resulted in notifications that stuck around forever, requiring the user
- // to manually close them. This introduces a minimum amount of time between calls,
- // and batches up the quick successive update() calls we get from an incoming
- // read sync, which might have a number of messages referenced inside of it.
- this.fastUpdate = this.update;
- this.update = _.debounce(this.update, 2000);
-
- // make those calls use the debounced function
- this.on('add', this.update);
- this.on('remove', this.onRemove);
- },
- update() {
- if (this.lastNotification) {
- this.lastNotification.close();
- this.lastNotification = null;
- }
-
- const { isEnabled } = this;
- const isAppFocused = isFocused();
- const isAudioNotificationEnabled = storage.get('audio-notification') || false;
- const isAudioNotificationSupported = Settings.isAudioNotificationSupported();
- // const isNotificationGroupingSupported = Settings.isNotificationGroupingSupported();
- const numNotifications = this.length;
- const userSetting = this.getUserSetting();
-
- const status = Signal.Notifications.getStatus({
- isAppFocused,
- isAudioNotificationEnabled,
- isAudioNotificationSupported,
- isEnabled,
- numNotifications,
- userSetting,
- });
-
- // window.log.info(
- // 'Update notifications:',
- // Object.assign({}, status, {
- // isNotificationGroupingSupported,
- // })
- // );
-
- if (status.type !== 'ok') {
- if (status.shouldClearNotifications) {
- this.reset([]);
- }
-
- return;
- }
-
- let title;
- let message;
- let iconUrl;
-
- const messagesNotificationCount = this.models.length;
-
- // NOTE: i18n has more complex rules for pluralization than just
- // distinguishing between zero (0) and other (non-zero),
- // e.g. Russian:
- // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
- const newMessageCountLabel = `${messagesNotificationCount} ${
- messagesNotificationCount === 1 ? i18n('newMessage') : i18n('newMessages')
- }`;
-
- const last = this.last().toJSON();
- switch (userSetting) {
- case SettingNames.COUNT:
- title = 'Session';
-
- if (messagesNotificationCount > 0) {
- message = newMessageCountLabel;
- } else {
- return;
- }
- break;
- case SettingNames.NAME: {
- const lastMessageTitle = last.title;
- title = newMessageCountLabel;
- // eslint-disable-next-line prefer-destructuring
- iconUrl = last.iconUrl;
- if (messagesNotificationCount === 1) {
- message = `${i18n('notificationFrom')} ${lastMessageTitle}`;
- } else {
- message = i18n('notificationMostRecentFrom', lastMessageTitle);
- }
- break;
- }
- case SettingNames.MESSAGE:
- if (messagesNotificationCount === 1) {
- // eslint-disable-next-line prefer-destructuring
- title = last.title;
- // eslint-disable-next-line prefer-destructuring
- message = last.message;
- } else {
- title = newMessageCountLabel;
- message = `${i18n('notificationMostRecent')} ${last.message}`;
- }
- // eslint-disable-next-line prefer-destructuring
- iconUrl = last.iconUrl;
- break;
- default:
- window.log.error(`Error: Unknown user notification setting: '${userSetting}'`);
- break;
- }
-
- const shouldHideExpiringMessageBody = last.isExpiringMessage && Signal.OS.isMacOS();
- if (shouldHideExpiringMessageBody) {
- message = i18n('newMessage');
- }
-
- drawAttention();
- this.lastNotification = new Notification(title, {
- body: window.platform === 'linux' ? filter(message) : message,
- icon: iconUrl,
- silent: !status.shouldPlayNotificationSound,
- });
- this.lastNotification.onclick = () => this.trigger('click', last.conversationId, last.id);
-
- // We continue to build up more and more messages for our notifications
- // until the user comes back to our app or closes the app. Then we’ll
- // clear everything out. The good news is that we'll have a maximum of
- // 1 notification in the Notification area (something like
- // ‘10 new messages’) assuming that `Notification::close` does its job.
- },
- getUserSetting() {
- return storage.get('notification-setting') || SettingNames.MESSAGE;
- },
- onRemove() {
- // window.log.info('Remove notification');
- this.update();
- },
- clear() {
- // window.log.info('Remove all notifications');
- this.reset([]);
- this.update();
- },
- // We don't usually call this, but when the process is shutting down, we should at
- // least try to remove the notification immediately instead of waiting for the
- // normal debounce.
- fastClear() {
- this.reset([]);
- this.fastUpdate();
- },
- enable() {
- const needUpdate = !this.isEnabled;
- this.isEnabled = true;
- if (needUpdate) {
- this.update();
- }
- },
- disable() {
- this.isEnabled = false;
- },
- }))();
-})();
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 20be3cc2e..fd94a6776 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -63,6 +63,7 @@ import {
} from '../types/MessageAttachment';
import { getOurPubKeyStrFromCache } from '../session/utils/User';
import { MessageRequestResponse } from '../session/messages/outgoing/controlMessage/MessageRequestResponse';
+import { Notifications } from '../util/notifications';
export enum ConversationTypeEnum {
GROUP = 'group',
@@ -1066,11 +1067,7 @@ export class ConversationModel extends Backbone.Model {
_.defaults(options, { sendReadReceipts: true });
const conversationId = this.id;
- window.Whisper.Notifications.remove(
- window.Whisper.Notifications.where({
- conversationId,
- })
- );
+ Notifications.clearByConversationID(conversationId);
let allUnreadMessagesInConvo = (await this.getUnread()).models;
const oldUnreadNowRead = allUnreadMessagesInConvo.filter(
@@ -1538,7 +1535,7 @@ export class ConversationModel extends Backbone.Model {
// isExpiringMessage,
// messageSentAt,
// });
- window.Whisper.Notifications.add({
+ Notifications.addNotification({
conversationId,
iconUrl,
isExpiringMessage,
@@ -1569,7 +1566,7 @@ export class ConversationModel extends Backbone.Model {
const now = Date.now();
const iconUrl = await this.getNotificationIcon();
- window.Whisper.Notifications.add({
+ Notifications.addNotification({
conversationId,
iconUrl,
isExpiringMessage: false,
diff --git a/ts/models/message.ts b/ts/models/message.ts
index f5a1c9cbe..9148c15eb 100644
--- a/ts/models/message.ts
+++ b/ts/models/message.ts
@@ -61,6 +61,7 @@ import {
loadQuoteData,
} from '../types/MessageAttachment';
import { ExpirationTimerOptions } from '../util/expiringMessages';
+import { Notifications } from '../util/notifications';
// tslint:disable: cyclomatic-complexity
/**
@@ -1088,11 +1089,7 @@ export class MessageModel extends Backbone.Model {
this.set({ expirationStartTimestamp });
}
- window.Whisper.Notifications.remove(
- window.Whisper.Notifications.where({
- messageId: this.id,
- })
- );
+ Notifications.clearByMessageId(this.id);
}
public isExpiring() {
diff --git a/ts/util/notifications.ts b/ts/util/notifications.ts
new file mode 100644
index 000000000..97f3e8528
--- /dev/null
+++ b/ts/util/notifications.ts
@@ -0,0 +1,239 @@
+import _ from 'lodash';
+import { getStatus } from '../notifications';
+import { isMacOS } from '../OS';
+import { isAudioNotificationSupported } from '../types/Settings';
+import { isWindowFocused } from './focusListener';
+
+const SettingNames = {
+ COUNT: 'count',
+ NAME: 'name',
+ MESSAGE: 'message',
+};
+
+function filter(text?: string) {
+ return (text || '')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(//g, '>');
+}
+
+export type SessionNotification = {
+ conversationId: string;
+ iconUrl: string;
+ isExpiringMessage: boolean;
+ message: string;
+ messageId?: string;
+ messageSentAt: number;
+ title: string;
+};
+
+let isEnabled: boolean = false;
+let lastNotificationDisplayed: null | Notification = null;
+
+let currentNotifications: Array = [];
+
+// Testing indicated that trying to create/destroy notifications too quickly
+// resulted in notifications that stuck around forever, requiring the user
+// to manually close them. This introduces a minimum amount of time between calls,
+// and batches up the quick successive update() calls we get from an incoming
+// read sync, which might have a number of messages referenced inside of it.
+const debouncedUpdate = _.debounce(update, 2000);
+const fastUpdate = update;
+
+function clear() {
+ // window.log.info('Remove all notifications');
+ currentNotifications = [];
+ debouncedUpdate();
+}
+
+// We don't usually call this, but when the process is shutting down, we should at
+// least try to remove the notification immediately instead of waiting for the
+// normal debounce.
+function fastClear() {
+ currentNotifications = [];
+ fastUpdate();
+}
+
+function enable() {
+ const needUpdate = !isEnabled;
+ isEnabled = true;
+ if (needUpdate) {
+ debouncedUpdate();
+ }
+}
+
+function disable() {
+ isEnabled = false;
+}
+
+function addNotification(notif: SessionNotification) {
+ const alreadyThere = currentNotifications.find(
+ n => n.conversationId === notif.conversationId && n.messageId === notif.messageId
+ );
+
+ if (alreadyThere) {
+ return;
+ }
+ currentNotifications.push(notif);
+ debouncedUpdate();
+}
+
+function clearByConversationID(convoId: string) {
+ const oldLength = currentNotifications.length;
+ currentNotifications = currentNotifications.filter(n => n.conversationId === convoId);
+ if (oldLength !== currentNotifications.length) {
+ onRemove();
+ }
+}
+
+function clearByMessageId(messageId: string) {
+ if (!messageId) {
+ return;
+ }
+ const oldLength = currentNotifications.length;
+ currentNotifications = currentNotifications.filter(n => n.messageId === messageId);
+ if (oldLength !== currentNotifications.length) {
+ onRemove();
+ }
+}
+
+function update() {
+ if (lastNotificationDisplayed) {
+ lastNotificationDisplayed.close();
+ lastNotificationDisplayed = null;
+ }
+
+ const isAppFocused = isWindowFocused();
+ const isAudioNotificationEnabled = storage.get('audio-notification') || false;
+ const audioNotificationSupported = isAudioNotificationSupported();
+ // const isNotificationGroupingSupported = Settings.isNotificationGroupingSupported();
+ const numNotifications = currentNotifications.length;
+ const userSetting = getUserSetting();
+
+ const status = getStatus({
+ isAppFocused,
+ isAudioNotificationEnabled,
+ isAudioNotificationSupported: audioNotificationSupported,
+ isEnabled,
+ numNotifications,
+ userSetting,
+ });
+
+ // window.log.info(
+ // 'Update notifications:',
+ // Object.assign({}, status, {
+ // isNotificationGroupingSupported,
+ // })
+ // );
+
+ if (status.type !== 'ok') {
+ if (status.shouldClearNotifications) {
+ currentNotifications = [];
+ }
+
+ return;
+ }
+
+ let title;
+ let message;
+ let iconUrl;
+
+ const messagesNotificationCount = currentNotifications.length;
+
+ // NOTE: i18n has more complex rules for pluralization than just
+ // distinguishing between zero (0) and other (non-zero),
+ // e.g. Russian:
+ // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
+ const newMessageCountLabel = `${messagesNotificationCount} ${
+ messagesNotificationCount === 1 ? window.i18n('newMessage') : window.i18n('newMessages')
+ }`;
+
+ if (!currentNotifications.length) {
+ return;
+ }
+
+ const lastNotification = _.last(currentNotifications);
+
+ if (!lastNotification) {
+ return;
+ }
+
+ switch (userSetting) {
+ case SettingNames.COUNT:
+ title = 'Session';
+
+ if (messagesNotificationCount > 0) {
+ message = newMessageCountLabel;
+ } else {
+ return;
+ }
+ break;
+ case SettingNames.NAME: {
+ const lastMessageTitle = lastNotification.title;
+ title = newMessageCountLabel;
+ // eslint-disable-next-line prefer-destructuring
+ iconUrl = lastNotification.iconUrl;
+ if (messagesNotificationCount === 1) {
+ message = `${window.i18n('notificationFrom')} ${lastMessageTitle}`;
+ } else {
+ message = window.i18n('notificationMostRecentFrom', [lastMessageTitle]);
+ }
+ break;
+ }
+ case SettingNames.MESSAGE:
+ if (messagesNotificationCount === 1) {
+ // eslint-disable-next-line prefer-destructuring
+ title = lastNotification.title;
+ // eslint-disable-next-line prefer-destructuring
+ message = lastNotification.message;
+ } else {
+ title = newMessageCountLabel;
+ message = `${window.i18n('notificationMostRecent')} ${lastNotification.message}`;
+ }
+ // eslint-disable-next-line prefer-destructuring
+ iconUrl = lastNotification.iconUrl;
+ break;
+ default:
+ window.log.error(`Error: Unknown user notification setting: '${userSetting}'`);
+ }
+
+ const shouldHideExpiringMessageBody = lastNotification.isExpiringMessage && isMacOS();
+ if (shouldHideExpiringMessageBody) {
+ message = window.i18n('newMessage');
+ }
+
+ window.drawAttention();
+ lastNotificationDisplayed = new Notification(title || '', {
+ body: window.platform === 'linux' ? filter(message) : message,
+ icon: iconUrl,
+ silent: !status.shouldPlayNotificationSound,
+ });
+ lastNotificationDisplayed.onclick = () => {
+ window.openFromNotification(lastNotification.conversationId);
+ };
+
+ // We continue to build up more and more messages for our notifications
+ // until the user comes back to our app or closes the app. Then we’ll
+ // clear everything out. The good news is that we'll have a maximum of
+ // 1 notification in the Notification area (something like
+ // ‘10 new messages’) assuming that `Notification::close` does its job.
+}
+function getUserSetting() {
+ return storage.get('notification-setting') || SettingNames.MESSAGE;
+}
+function onRemove() {
+ // window.log.info('Remove notification');
+ debouncedUpdate();
+}
+
+export const Notifications = {
+ addNotification,
+ disable,
+ enable,
+ clear,
+ fastClear,
+ clearByConversationID,
+ clearByMessageId,
+};
diff --git a/ts/window.d.ts b/ts/window.d.ts
index bfb8aa8c7..8c70cefc3 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -60,8 +60,12 @@ declare global {
versionInfo: any;
getConversations: () => ConversationCollection;
readyForUpdates: () => void;
+ drawAttention: () => void;
MediaRecorder: any;
+ platform: string;
+ openFromNotification: (convoId: string) => void;
+
contextMenuShown: boolean;
inboxStore?: Store;
openConversationWithMessages: (args: {