From 747bcb766ce3154a8621daffdfd7c02e8e35e8b7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 23 Mar 2022 14:27:10 +1100 Subject: [PATCH] move notifications.js to ts --- background.html | 5 - js/background.js | 3 +- js/modules/signal.js | 2 - js/notifications.js | 194 ------------------------------- ts/models/conversation.ts | 11 +- ts/models/message.ts | 7 +- ts/util/notifications.ts | 239 ++++++++++++++++++++++++++++++++++++++ ts/window.d.ts | 4 + 8 files changed, 251 insertions(+), 214 deletions(-) delete mode 100644 js/notifications.js create mode 100644 ts/util/notifications.ts 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: {