Notification improvements

This commit is contained in:
Evan Hahn 2021-09-23 13:16:09 -05:00 committed by GitHub
parent 04a4e6e5ff
commit d2ef82686d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 408 additions and 410 deletions

View file

@ -14,7 +14,6 @@ const EmojiLib = require('../../ts/components/emoji/lib');
const Groups = require('../../ts/groups');
const GroupChange = require('../../ts/groupChange');
const IndexedDB = require('./indexeddb');
const Notifications = require('../../ts/notifications');
const OS = require('../../ts/OS');
const Stickers = require('../../ts/types/Stickers');
const Settings = require('./settings');
@ -159,7 +158,6 @@ const {
const {
initializeUpdateListener,
} = require('../../ts/services/updateListener');
const { notify } = require('../../ts/services/notify');
const { calling } = require('../../ts/services/calling');
const { onTimeout, removeTimeout } = require('../../ts/services/timers');
const {
@ -406,7 +404,6 @@ exports.setup = (options = {}) => {
initializeNetworkObserver,
initializeUpdateListener,
onTimeout,
notify,
removeTimeout,
runStorageServiceSyncJob,
storageServiceUploadJob,
@ -454,7 +451,6 @@ exports.setup = (options = {}) => {
GroupChange,
IndexedDB,
Migrations,
Notifications,
OS,
RemoteConfig,
Settings,

View file

@ -1,251 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global Signal:false */
/* global Backbone: false */
/* global drawAttention: false */
/* global i18n: false */
/* global storage: false */
/* global Whisper: false */
/* global _: false */
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
// The keys and values don't match here. This is because the values correspond to old
// setting names. In the future, we may wish to migrate these to match.
const SettingNames = {
NO_NAME_OR_MESSAGE: 'count',
NAME_ONLY: 'name',
NAME_AND_MESSAGE: 'message',
};
// Electron, at least on Windows and macOS, only shows one notification at a time (see
// issues [#15364][0] and [#21646][1], among others). Because of that, we have a
// single slot for notifications, and once a notification is dismissed, all of
// Signal's notifications are dismissed.
// [0]: https://github.com/electron/electron/issues/15364
// [1]: https://github.com/electron/electron/issues/21646
Whisper.Notifications = {
...Backbone.Events,
isEnabled: false,
// This is either a standard `Notification` or null.
lastNotification: null,
// This is either null or an object of this shape:
//
// {
// conversationId: string;
// messageId: string;
// senderTitle: string;
// message: string;
// notificationIconUrl: string | void;
// isExpiringMessage: boolean;
// reaction: {
// emoji: string;
// fromId: string;
// };
// }
notificationData: null,
add(notificationData) {
this.notificationData = notificationData;
this.update();
},
// Remove the last notification if both conditions hold:
//
// 1. Either `conversationId` or `messageId` matches (if present)
// 2. `emoji`, `targetAuthorUuid`, `targetTimestamp` matches (if present)
removeBy({
conversationId,
messageId,
emoji,
targetAuthorUuid,
targetTimestamp,
}) {
if (!this.notificationData) {
return;
}
let shouldClear = false;
if (
conversationId &&
this.notificationData.conversationId === conversationId
) {
shouldClear = true;
}
if (messageId && this.notificationData.messageId === messageId) {
shouldClear = true;
}
if (!shouldClear) {
return;
}
const { reaction } = this.notificationData;
if (
reaction &&
emoji &&
targetAuthorUuid &&
targetTimestamp &&
(reaction.emoji !== emoji ||
reaction.targetAuthorUuid !== targetAuthorUuid ||
reaction.targetTimestamp !== targetTimestamp)
) {
return;
}
this.clear();
this.update();
},
fastUpdate() {
if (this.lastNotification) {
this.lastNotification.close();
this.lastNotification = null;
}
const { isEnabled } = this;
const isAppFocused = window.isActive();
const isAudioNotificationEnabled =
storage.get('audio-notification') || false;
const userSetting = this.getUserSetting();
const status = Signal.Notifications.getStatus({
isAppFocused,
isAudioNotificationEnabled,
isEnabled,
hasNotifications: Boolean(this.notificationData),
userSetting,
});
const shouldDrawAttention = storage.get(
'notification-draw-attention',
true
);
if (shouldDrawAttention) {
drawAttention();
}
if (status.type !== 'ok') {
window.SignalWindow.log.info(
`Not updating notifications; notification status is ${status.type}. ${
status.shouldClearNotifications ? 'Also clearing notifications' : ''
}`
);
if (status.shouldClearNotifications) {
this.notificationData = null;
}
return;
}
window.SignalWindow.log.info('Showing a notification');
let notificationTitle;
let notificationMessage;
let notificationIconUrl;
const {
conversationId,
messageId,
senderTitle,
message,
isExpiringMessage,
reaction,
} = this.notificationData;
if (
userSetting === SettingNames.NAME_ONLY ||
userSetting === SettingNames.NAME_AND_MESSAGE
) {
notificationTitle = senderTitle;
({ notificationIconUrl } = this.notificationData);
const shouldHideExpiringMessageBody =
isExpiringMessage && (Signal.OS.isMacOS() || Signal.OS.isWindows());
if (shouldHideExpiringMessageBody) {
notificationMessage = i18n('newMessage');
} else if (userSetting === SettingNames.NAME_ONLY) {
if (reaction) {
notificationMessage = i18n('notificationReaction', {
sender: senderTitle,
emoji: reaction.emoji,
});
} else {
notificationMessage = i18n('newMessage');
}
} else if (reaction) {
notificationMessage = i18n('notificationReactionMessage', {
sender: senderTitle,
emoji: reaction.emoji,
message,
});
} else {
notificationMessage = message;
}
} else {
if (userSetting !== SettingNames.NO_NAME_OR_MESSAGE) {
window.SignalWindow.log.error(
`Error: Unknown user notification setting: '${userSetting}'`
);
}
notificationTitle = 'Signal';
notificationMessage = i18n('newMessage');
}
this.lastNotification = window.Signal.Services.notify({
title: notificationTitle,
icon: notificationIconUrl,
message: notificationMessage,
silent: !status.shouldPlayNotificationSound,
onNotificationClick: () => {
this.trigger('click', conversationId, messageId);
},
});
},
getUserSetting() {
return (
storage.get('notification-setting') || SettingNames.NAME_AND_MESSAGE
);
},
clear() {
window.SignalWindow.log.info('Removing notification');
this.notificationData = null;
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.notificationData = null;
this.fastUpdate();
},
enable() {
const needUpdate = !this.isEnabled;
this.isEnabled = true;
if (needUpdate) {
this.update();
}
},
disable() {
this.isEnabled = false;
},
};
// 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.
Whisper.Notifications.update = _.debounce(
Whisper.Notifications.fastUpdate.bind(Whisper.Notifications),
1000
);
})();

View file

@ -38,6 +38,7 @@ import { updateConversationsWithUuidLookup } from './updateConversationsWithUuid
import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
import { ourProfileKeyService } from './services/ourProfileKey';
import { notificationService } from './services/notifications';
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
import { LatestQueue } from './util/LatestQueue';
import { parseIntOrThrow } from './util/parseIntOrThrow';
@ -139,6 +140,10 @@ export async function startApp(): Promise<void> {
window.Signal.Util.MessageController.install();
window.Signal.conversationControllerStart();
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
notificationService.initialize({
i18n: window.i18n,
storage: window.storage,
});
window.attachmentDownloadQueue = [];
try {
log.info('Initializing SQL in renderer');
@ -1768,12 +1773,10 @@ export async function startApp(): Promise<void> {
}
});
window.registerForActive(() => window.Whisper.Notifications.clear());
window.addEventListener('unload', () =>
window.Whisper.Notifications.fastClear()
);
window.registerForActive(() => notificationService.clear());
window.addEventListener('unload', () => notificationService.fastClear());
window.Whisper.Notifications.on('click', (id, messageId) => {
notificationService.on('click', (id, messageId) => {
window.showWindow();
if (id) {
window.Whisper.events.trigger('showConversation', id, messageId);
@ -2062,7 +2065,7 @@ export async function startApp(): Promise<void> {
profileKeyResponseQueue.pause();
lightSessionResetQueue.pause();
window.Whisper.deliveryReceiptQueue.pause();
window.Whisper.Notifications.disable();
notificationService.disable();
window.Signal.Services.initializeGroupCredentialFetcher();
@ -2314,7 +2317,7 @@ export async function startApp(): Promise<void> {
profileKeyResponseQueue.start();
lightSessionResetQueue.start();
window.Whisper.deliveryReceiptQueue.start();
window.Whisper.Notifications.enable();
notificationService.enable();
await onAppView;
@ -2378,7 +2381,7 @@ export async function startApp(): Promise<void> {
profileKeyResponseQueue.pause();
lightSessionResetQueue.pause();
window.Whisper.deliveryReceiptQueue.pause();
window.Whisper.Notifications.disable();
notificationService.disable();
}
let initialStartupCount = 0;

View file

@ -1,4 +1,4 @@
// Copyright 2017-2020 Signal Messenger, LLC
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
@ -8,6 +8,7 @@ import { Collection, Model } from 'backbone';
import { MessageModel } from '../models/messages';
import { isIncoming } from '../state/selectors/message';
import { isMessageUnread } from '../util/isMessageUnread';
import { notificationService } from '../services/notifications';
import * as log from '../logging/log';
type ReadSyncAttributesType = {
@ -39,7 +40,7 @@ async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
return;
}
window.Whisper.Notifications.removeBy({
notificationService.removeBy({
conversationId: readReaction.conversationId,
emoji: readReaction.emoji,
targetAuthorUuid: readReaction.targetAuthorUuid,
@ -99,7 +100,7 @@ export class ReadSyncs extends Collection {
return;
}
window.Whisper.Notifications.removeBy({ messageId: found.id });
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);
const readAt = Math.min(sync.get('readAt'), Date.now());

View file

@ -9,6 +9,7 @@ import { MessageModel } from '../models/messages';
import { ReadStatus } from '../messages/MessageReadStatus';
import { markViewed } from '../services/MessageUpdater';
import { isIncoming } from '../state/selectors/message';
import { notificationService } from '../services/notifications';
import * as log from '../logging/log';
type ViewSyncAttributesType = {
@ -83,7 +84,7 @@ export class ViewSyncs extends Collection {
return;
}
window.Whisper.Notifications.removeBy({ messageId: found.id });
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);

View file

@ -54,6 +54,7 @@ import { migrateColor } from '../util/migrateColor';
import { isNotNil } from '../util/isNotNil';
import { dropNull } from '../util/dropNull';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { notificationService } from '../services/notifications';
import { getSendOptions } from '../util/getSendOptions';
import { isConversationAccepted } from '../util/isConversationAccepted';
import { markConversationRead } from '../util/markConversationRead';
@ -4847,7 +4848,7 @@ export class ConversationModel extends window.Backbone
): Promise<void> {
// As a performance optimization don't perform any work if notifications are
// disabled.
if (!window.Whisper.Notifications.isEnabled) {
if (!notificationService.isEnabled) {
return;
}
@ -4900,7 +4901,7 @@ export class ConversationModel extends window.Backbone
const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON);
window.Whisper.Notifications.add({
notificationService.add({
senderTitle,
conversationId,
notificationIconUrl,

View file

@ -112,6 +112,7 @@ import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
import * as LinkPreview from '../types/LinkPreview';
import { SignalService as Proto } from '../protobuf';
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
import { notificationService } from '../services/notifications';
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
import * as log from '../logging/log';
@ -3292,7 +3293,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
// Remove any notifications for this message
window.Whisper.Notifications.removeBy({ messageId: this.get('id') });
notificationService.removeBy({ messageId: this.get('id') });
// Erase the contents of this message
await this.eraseContents(
@ -3306,7 +3307,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
clearNotifications(reaction: Partial<ReactionType> = {}): void {
window.Whisper.Notifications.removeBy({
notificationService.removeBy({
...reaction,
messageId: this.id,
});

View file

@ -1,61 +0,0 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
type Environment = {
isAppFocused: boolean;
isAudioNotificationEnabled: boolean;
isEnabled: boolean;
hasNotifications: boolean;
userSetting: UserSetting;
};
type Status = {
shouldClearNotifications: boolean;
shouldPlayNotificationSound: boolean;
shouldShowNotifications: boolean;
type: Type;
};
type UserSetting = 'off' | 'count' | 'name' | 'message';
type Type =
| 'ok'
| 'disabled'
| 'appIsFocused'
| 'noNotifications'
| 'userSetting';
export const getStatus = ({
isAppFocused,
isAudioNotificationEnabled,
isEnabled,
hasNotifications,
userSetting,
}: Environment): Status => {
const type = ((): Type => {
if (!isEnabled) {
return 'disabled';
}
if (!hasNotifications) {
return 'noNotifications';
}
if (isAppFocused) {
return 'appIsFocused';
}
if (userSetting === 'off') {
return 'userSetting';
}
return 'ok';
})();
return {
shouldClearNotifications: type === 'appIsFocused',
shouldPlayNotificationSound: isAudioNotificationEnabled,
shouldShowNotifications: type === 'ok',
type,
};
};

View file

@ -1,6 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getStatus } from './getStatus';
export { getStatus };

View file

@ -3,6 +3,7 @@
import type { MessageAttributesType } from '../model-types.d';
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
import { notificationService } from './notifications';
function markReadOrViewed(
messageAttrs: Readonly<MessageAttributesType>,
@ -27,7 +28,7 @@ function markReadOrViewed(
);
}
window.Whisper.Notifications.removeBy({ messageId });
notificationService.removeBy({ messageId });
if (!skipSave) {
window.Signal.Util.queueUpdateMessage(nextMessageAttributes);

View file

@ -80,10 +80,10 @@ import {
REQUESTED_VIDEO_FRAMERATE,
} from '../calling/constants';
import { callingMessageToProto } from '../util/callingMessageToProto';
import { notify } from './notify';
import { getSendOptions } from '../util/getSendOptions';
import { SignalService as Proto } from '../protobuf';
import dataInterface from '../sql/Client';
import { notificationService } from './notifications';
import * as log from '../logging/log';
const {
@ -1117,7 +1117,7 @@ export class CallingClass {
if (source) {
ipcRenderer.send('show-screen-share', source.name);
notify({
notificationService.notify({
icon: 'images/icons/v2/video-solid-24.svg',
message: window.i18n('calling__presenting--notification-body'),
onNotificationClick: () => {

View file

@ -0,0 +1,352 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { debounce } from 'lodash';
import EventEmitter from 'events';
import { Sound } from '../util/Sound';
import {
AudioNotificationSupport,
getAudioNotificationSupport,
} from '../types/Settings';
import * as OS from '../OS';
import * as log from '../logging/log';
import { makeEnumParser } from '../util/enum';
import type { StorageInterface } from '../types/Storage.d';
import type { LocalizerType } from '../types/Util';
type NotificationDataType = {
conversationId: string;
messageId: string;
senderTitle: string;
message: string;
notificationIconUrl?: undefined | string;
isExpiringMessage: boolean;
reaction: {
emoji: string;
targetAuthorUuid: string;
targetTimestamp: number;
};
};
// The keys and values don't match here. This is because the values correspond to old
// setting names. In the future, we may wish to migrate these to match.
export enum NotificationSetting {
Off = 'off',
NoNameOrMessage = 'count',
NameOnly = 'name',
NameAndMessage = 'message',
}
const parseNotificationSetting = makeEnumParser(
NotificationSetting,
NotificationSetting.NameAndMessage
);
export const FALLBACK_NOTIFICATION_TITLE = 'Signal';
// Electron, at least on Windows and macOS, only shows one notification at a time (see
// issues [#15364][0] and [#21646][1], among others). Because of that, we have a
// single slot for notifications, and once a notification is dismissed, all of
// Signal's notifications are dismissed.
// [0]: https://github.com/electron/electron/issues/15364
// [1]: https://github.com/electron/electron/issues/21646
class NotificationService extends EventEmitter {
private i18n?: LocalizerType;
private storage?: StorageInterface;
public isEnabled = false;
private lastNotification: null | Notification = null;
private notificationData: null | NotificationDataType = 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.
private update: () => unknown;
constructor() {
super();
this.update = debounce(this.fastUpdate.bind(this), 1000);
}
public initialize({
i18n,
storage,
}: Readonly<{ i18n: LocalizerType; storage: StorageInterface }>): void {
this.i18n = i18n;
this.storage = storage;
}
private getStorage(): StorageInterface {
if (this.storage) {
return this.storage;
}
log.error(
'NotificationService not initialized. Falling back to window.storage, but you should fix this'
);
return window.storage;
}
private getI18n(): LocalizerType {
if (this.i18n) {
return this.i18n;
}
log.error(
'NotificationService not initialized. Falling back to window.i18n, but you should fix this'
);
return window.i18n;
}
/**
* A higher-level wrapper around `window.Notification`. You may prefer to use `notify`,
* which doesn't check permissions, do any filtering, etc.
*/
public add(notificationData: NotificationDataType): void {
this.notificationData = notificationData;
this.update();
}
/**
* A lower-level wrapper around `window.Notification`. You may prefer to use `add`,
* which includes debouncing and user permission logic.
*/
public notify({
icon,
message,
onNotificationClick,
silent,
title,
}: Readonly<{
icon?: string;
message: string;
onNotificationClick: () => void;
silent: boolean;
title: string;
}>): void {
this.lastNotification?.close();
const audioNotificationSupport = getAudioNotificationSupport();
const notification = new window.Notification(title, {
body: OS.isLinux() ? filterNotificationText(message) : message,
icon,
silent:
silent || audioNotificationSupport !== AudioNotificationSupport.Native,
});
notification.onclick = onNotificationClick;
if (
!silent &&
audioNotificationSupport === AudioNotificationSupport.Custom
) {
// We kick off the sound to be played. No need to await it.
new Sound({ src: 'sounds/notification.ogg' }).play();
}
this.lastNotification = notification;
}
// Remove the last notification if both conditions hold:
//
// 1. Either `conversationId` or `messageId` matches (if present)
// 2. `emoji`, `targetAuthorUuid`, `targetTimestamp` matches (if present)
public removeBy({
conversationId,
messageId,
emoji,
targetAuthorUuid,
targetTimestamp,
}: Readonly<{
conversationId?: string;
messageId?: string;
emoji?: string;
targetAuthorUuid?: string;
targetTimestamp?: number;
}>): void {
if (!this.notificationData) {
return;
}
let shouldClear = false;
if (
conversationId &&
this.notificationData.conversationId === conversationId
) {
shouldClear = true;
}
if (messageId && this.notificationData.messageId === messageId) {
shouldClear = true;
}
if (!shouldClear) {
return;
}
const { reaction } = this.notificationData;
if (
reaction &&
emoji &&
targetAuthorUuid &&
targetTimestamp &&
(reaction.emoji !== emoji ||
reaction.targetAuthorUuid !== targetAuthorUuid ||
reaction.targetTimestamp !== targetTimestamp)
) {
return;
}
this.clear();
this.update();
}
private fastUpdate(): void {
const storage = this.getStorage();
const i18n = this.getI18n();
if (this.lastNotification) {
this.lastNotification.close();
this.lastNotification = null;
}
const { notificationData } = this;
const isAppFocused = window.isActive();
const userSetting = this.getNotificationSetting();
// This isn't a boolean because TypeScript isn't smart enough to know that, if
// `Boolean(notificationData)` is true, `notificationData` is truthy.
const shouldShowNotification =
this.isEnabled &&
!isAppFocused &&
notificationData &&
userSetting !== NotificationSetting.Off;
if (!shouldShowNotification) {
log.info('Not updating notifications');
if (isAppFocused) {
this.notificationData = null;
}
return;
}
log.info('Showing a notification');
const shouldDrawAttention = storage.get(
'notification-draw-attention',
true
);
if (shouldDrawAttention) {
window.drawAttention();
}
let notificationTitle: string;
let notificationMessage: string;
let notificationIconUrl: undefined | string;
const {
conversationId,
messageId,
senderTitle,
message,
isExpiringMessage,
reaction,
} = notificationData;
if (
userSetting === NotificationSetting.NameOnly ||
userSetting === NotificationSetting.NameAndMessage
) {
notificationTitle = senderTitle;
({ notificationIconUrl } = notificationData);
const shouldHideExpiringMessageBody =
isExpiringMessage && (OS.isMacOS() || OS.isWindows());
if (shouldHideExpiringMessageBody) {
notificationMessage = i18n('newMessage');
} else if (userSetting === NotificationSetting.NameOnly) {
if (reaction) {
notificationMessage = i18n('notificationReaction', {
sender: senderTitle,
emoji: reaction.emoji,
});
} else {
notificationMessage = i18n('newMessage');
}
} else if (reaction) {
notificationMessage = i18n('notificationReactionMessage', {
sender: senderTitle,
emoji: reaction.emoji,
message,
});
} else {
notificationMessage = message;
}
} else {
if (userSetting !== NotificationSetting.NoNameOrMessage) {
window.SignalWindow.log.error(
`Error: Unknown user notification setting: '${userSetting}'`
);
}
notificationTitle = FALLBACK_NOTIFICATION_TITLE;
notificationMessage = i18n('newMessage');
}
this.notify({
title: notificationTitle,
icon: notificationIconUrl,
message: notificationMessage,
silent: Boolean(storage.get('audio-notification')),
onNotificationClick: () => {
this.emit('click', conversationId, messageId);
},
});
}
public getNotificationSetting(): NotificationSetting {
return parseNotificationSetting(
this.getStorage().get('notification-setting')
);
}
public clear(): void {
window.SignalWindow.log.info('Removing notification');
this.notificationData = null;
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.
public fastClear(): void {
this.notificationData = null;
this.fastUpdate();
}
public enable(): void {
const needUpdate = !this.isEnabled;
this.isEnabled = true;
if (needUpdate) {
this.update();
}
}
public disable(): void {
this.isEnabled = false;
}
}
export const notificationService = new NotificationService();
function filterNotificationText(text: string) {
return (text || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

View file

@ -1,51 +0,0 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Sound } from '../util/Sound';
import {
AudioNotificationSupport,
getAudioNotificationSupport,
} from '../types/Settings';
import * as OS from '../OS';
function filter(text: string) {
return (text || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
type NotificationType = {
icon: string;
message: string;
onNotificationClick: () => void;
silent: boolean;
title: string;
};
export function notify({
icon,
message,
onNotificationClick,
silent,
title,
}: NotificationType): Notification {
const audioNotificationSupport = getAudioNotificationSupport();
const notification = new window.Notification(title, {
body: OS.isLinux() ? filter(message) : message,
icon,
silent:
silent || audioNotificationSupport !== AudioNotificationSupport.Native,
});
notification.onclick = onNotificationClick;
if (!silent && audioNotificationSupport === AudioNotificationSupport.Custom) {
// We kick off the sound to be played. No need to await it.
new Sound({ src: 'sounds/notification.ogg' }).play();
}
return notification;
}

View file

@ -26,12 +26,16 @@ import {
SmartSafetyNumberViewer,
Props as SafetyNumberViewerProps,
} from './SafetyNumberViewer';
import { notify } from '../../services/notify';
import { callingTones } from '../../util/callingTones';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
notificationService,
} from '../../services/notifications';
import * as log from '../../logging/log';
function renderDeviceSelection(): JSX.Element {
@ -55,8 +59,27 @@ async function notifyForCall(
if (!shouldNotify) {
return;
}
notify({
title,
let notificationTitle: string;
const notificationSetting = notificationService.getNotificationSetting();
switch (notificationSetting) {
case NotificationSetting.Off:
case NotificationSetting.NoNameOrMessage:
notificationTitle = FALLBACK_NOTIFICATION_TITLE;
break;
case NotificationSetting.NameOnly:
case NotificationSetting.NameAndMessage:
notificationTitle = title;
break;
default:
log.error(missingCaseError(notificationSetting));
notificationTitle = FALLBACK_NOTIFICATION_TITLE;
break;
}
notificationService.notify({
title: notificationTitle,
icon: isVideoCall
? 'images/icons/v2/video-solid-24.svg'
: 'images/icons/v2/phone-right-solid-24.svg',

View file

@ -5,6 +5,7 @@ import { ConversationAttributesType } from '../model-types.d';
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
import { hasErrors } from '../state/selectors/message';
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
import { notificationService } from '../services/notifications';
import * as log from '../logging/log';
export async function markConversationRead(
@ -39,7 +40,7 @@ export async function markConversationRead(
return false;
}
window.Whisper.Notifications.removeBy({ conversationId });
notificationService.removeBy({ conversationId });
const unreadReactionSyncData = new Map<
string,

15
ts/window.d.ts vendored
View file

@ -176,6 +176,7 @@ declare global {
baseAttachmentsPath: string;
baseStickersPath: string;
baseTempPath: string;
drawAttention: () => void;
enterKeyboardMode: () => void;
enterMouseMode: () => void;
getAccountManager: () => AccountManager;
@ -591,20 +592,6 @@ export type WhisperType = {
) => void;
};
Notifications: {
isEnabled: boolean;
removeBy: (filter: Partial<unknown>) => void;
add: (notification: unknown) => void;
clear: () => void;
disable: () => void;
enable: () => void;
fastClear: () => void;
on: (
event: string,
callback: (id: string, messageId: string) => void
) => void;
};
ExpiringMessagesListener: {
init: (events: Backbone.Events) => void;
update: () => void;