session-desktop/ts/util/notifications.ts

261 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { debounce, last } from 'lodash';
import { SettingsKey } from '../data/settings-key';
import { getStatus } from '../notifications';
import { UserSetting } from '../notifications/getStatus';
import { isMacOS } from '../OS';
import { isAudioNotificationSupported } from '../types/Settings';
import { isWindowFocused } from './focusListener';
import { Storage } from './storage';
const SettingNames = {
COUNT: 'count',
NAME: 'name',
MESSAGE: 'message',
};
function filter(text?: string) {
return (text || '')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
let sound: any;
export type SessionNotification = {
conversationId: string;
iconUrl: string | null;
isExpiringMessage: boolean;
message: string;
messageId?: string;
messageSentAt: number;
title: string;
};
let isEnabled: boolean = false;
let lastNotificationDisplayed: null | Notification = null;
let currentNotifications: Array<SessionNotification> = [];
// 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;
}
/**
*
* @param forceRefresh Should only be set when the user triggers a test notification from the settings
*/
function addNotification(notif: SessionNotification, forceRefresh = false) {
const alreadyThere = currentNotifications.find(
n => n.conversationId === notif.conversationId && n.messageId === notif.messageId
);
if (alreadyThere) {
return;
}
currentNotifications.push(notif);
if (forceRefresh) {
update(true);
} else {
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();
}
}
// tslint:disable-next-line: cyclomatic-complexity
function update(forceRefresh = false) {
if (lastNotificationDisplayed) {
lastNotificationDisplayed.close();
lastNotificationDisplayed = null;
}
const isAppFocused = isWindowFocused();
const isAudioNotificationEnabled =
(Storage.get(SettingsKey.settingsAudioNotification) as boolean) || false;
const audioNotificationSupported = isAudioNotificationSupported();
// const isNotificationGroupingSupported = Settings.isNotificationGroupingSupported();
const numNotifications = currentNotifications.length;
const userSetting = getUserSetting();
const status = getStatus({
isAppFocused: forceRefresh ? false : 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();
if (status.shouldPlayNotificationSound) {
if (!sound) {
sound = new Audio('sound/new_message.mp3');
}
void sound.play();
}
lastNotificationDisplayed = new Notification(title || '', {
body: window.platform === 'linux' ? filter(message) : message,
icon: iconUrl || undefined,
silent: true,
});
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 well
// 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') as UserSetting) || SettingNames.MESSAGE;
}
function onRemove() {
// window.log.info('Remove notification');
debouncedUpdate();
}
export const Notifications = {
addNotification,
disable,
enable,
clear,
fastClear,
clearByConversationID,
clearByMessageId,
};