Notification improvements
This commit is contained in:
parent
04a4e6e5ff
commit
d2ef82686d
16 changed files with 408 additions and 410 deletions
|
@ -14,7 +14,6 @@ const EmojiLib = require('../../ts/components/emoji/lib');
|
||||||
const Groups = require('../../ts/groups');
|
const Groups = require('../../ts/groups');
|
||||||
const GroupChange = require('../../ts/groupChange');
|
const GroupChange = require('../../ts/groupChange');
|
||||||
const IndexedDB = require('./indexeddb');
|
const IndexedDB = require('./indexeddb');
|
||||||
const Notifications = require('../../ts/notifications');
|
|
||||||
const OS = require('../../ts/OS');
|
const OS = require('../../ts/OS');
|
||||||
const Stickers = require('../../ts/types/Stickers');
|
const Stickers = require('../../ts/types/Stickers');
|
||||||
const Settings = require('./settings');
|
const Settings = require('./settings');
|
||||||
|
@ -159,7 +158,6 @@ const {
|
||||||
const {
|
const {
|
||||||
initializeUpdateListener,
|
initializeUpdateListener,
|
||||||
} = require('../../ts/services/updateListener');
|
} = require('../../ts/services/updateListener');
|
||||||
const { notify } = require('../../ts/services/notify');
|
|
||||||
const { calling } = require('../../ts/services/calling');
|
const { calling } = require('../../ts/services/calling');
|
||||||
const { onTimeout, removeTimeout } = require('../../ts/services/timers');
|
const { onTimeout, removeTimeout } = require('../../ts/services/timers');
|
||||||
const {
|
const {
|
||||||
|
@ -406,7 +404,6 @@ exports.setup = (options = {}) => {
|
||||||
initializeNetworkObserver,
|
initializeNetworkObserver,
|
||||||
initializeUpdateListener,
|
initializeUpdateListener,
|
||||||
onTimeout,
|
onTimeout,
|
||||||
notify,
|
|
||||||
removeTimeout,
|
removeTimeout,
|
||||||
runStorageServiceSyncJob,
|
runStorageServiceSyncJob,
|
||||||
storageServiceUploadJob,
|
storageServiceUploadJob,
|
||||||
|
@ -454,7 +451,6 @@ exports.setup = (options = {}) => {
|
||||||
GroupChange,
|
GroupChange,
|
||||||
IndexedDB,
|
IndexedDB,
|
||||||
Migrations,
|
Migrations,
|
||||||
Notifications,
|
|
||||||
OS,
|
OS,
|
||||||
RemoteConfig,
|
RemoteConfig,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
})();
|
|
|
@ -38,6 +38,7 @@ import { updateConversationsWithUuidLookup } from './updateConversationsWithUuid
|
||||||
import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
|
import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
|
||||||
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
||||||
import { ourProfileKeyService } from './services/ourProfileKey';
|
import { ourProfileKeyService } from './services/ourProfileKey';
|
||||||
|
import { notificationService } from './services/notifications';
|
||||||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||||
import { LatestQueue } from './util/LatestQueue';
|
import { LatestQueue } from './util/LatestQueue';
|
||||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||||
|
@ -139,6 +140,10 @@ export async function startApp(): Promise<void> {
|
||||||
window.Signal.Util.MessageController.install();
|
window.Signal.Util.MessageController.install();
|
||||||
window.Signal.conversationControllerStart();
|
window.Signal.conversationControllerStart();
|
||||||
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
|
window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
|
||||||
|
notificationService.initialize({
|
||||||
|
i18n: window.i18n,
|
||||||
|
storage: window.storage,
|
||||||
|
});
|
||||||
window.attachmentDownloadQueue = [];
|
window.attachmentDownloadQueue = [];
|
||||||
try {
|
try {
|
||||||
log.info('Initializing SQL in renderer');
|
log.info('Initializing SQL in renderer');
|
||||||
|
@ -1768,12 +1773,10 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.registerForActive(() => window.Whisper.Notifications.clear());
|
window.registerForActive(() => notificationService.clear());
|
||||||
window.addEventListener('unload', () =>
|
window.addEventListener('unload', () => notificationService.fastClear());
|
||||||
window.Whisper.Notifications.fastClear()
|
|
||||||
);
|
|
||||||
|
|
||||||
window.Whisper.Notifications.on('click', (id, messageId) => {
|
notificationService.on('click', (id, messageId) => {
|
||||||
window.showWindow();
|
window.showWindow();
|
||||||
if (id) {
|
if (id) {
|
||||||
window.Whisper.events.trigger('showConversation', id, messageId);
|
window.Whisper.events.trigger('showConversation', id, messageId);
|
||||||
|
@ -2062,7 +2065,7 @@ export async function startApp(): Promise<void> {
|
||||||
profileKeyResponseQueue.pause();
|
profileKeyResponseQueue.pause();
|
||||||
lightSessionResetQueue.pause();
|
lightSessionResetQueue.pause();
|
||||||
window.Whisper.deliveryReceiptQueue.pause();
|
window.Whisper.deliveryReceiptQueue.pause();
|
||||||
window.Whisper.Notifications.disable();
|
notificationService.disable();
|
||||||
|
|
||||||
window.Signal.Services.initializeGroupCredentialFetcher();
|
window.Signal.Services.initializeGroupCredentialFetcher();
|
||||||
|
|
||||||
|
@ -2314,7 +2317,7 @@ export async function startApp(): Promise<void> {
|
||||||
profileKeyResponseQueue.start();
|
profileKeyResponseQueue.start();
|
||||||
lightSessionResetQueue.start();
|
lightSessionResetQueue.start();
|
||||||
window.Whisper.deliveryReceiptQueue.start();
|
window.Whisper.deliveryReceiptQueue.start();
|
||||||
window.Whisper.Notifications.enable();
|
notificationService.enable();
|
||||||
|
|
||||||
await onAppView;
|
await onAppView;
|
||||||
|
|
||||||
|
@ -2378,7 +2381,7 @@ export async function startApp(): Promise<void> {
|
||||||
profileKeyResponseQueue.pause();
|
profileKeyResponseQueue.pause();
|
||||||
lightSessionResetQueue.pause();
|
lightSessionResetQueue.pause();
|
||||||
window.Whisper.deliveryReceiptQueue.pause();
|
window.Whisper.deliveryReceiptQueue.pause();
|
||||||
window.Whisper.Notifications.disable();
|
notificationService.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialStartupCount = 0;
|
let initialStartupCount = 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2017-2020 Signal Messenger, LLC
|
// Copyright 2017-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
|
@ -8,6 +8,7 @@ import { Collection, Model } from 'backbone';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { isIncoming } from '../state/selectors/message';
|
import { isIncoming } from '../state/selectors/message';
|
||||||
import { isMessageUnread } from '../util/isMessageUnread';
|
import { isMessageUnread } from '../util/isMessageUnread';
|
||||||
|
import { notificationService } from '../services/notifications';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
type ReadSyncAttributesType = {
|
type ReadSyncAttributesType = {
|
||||||
|
@ -39,7 +40,7 @@ async function maybeItIsAReactionReadSync(sync: ReadSyncModel): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Notifications.removeBy({
|
notificationService.removeBy({
|
||||||
conversationId: readReaction.conversationId,
|
conversationId: readReaction.conversationId,
|
||||||
emoji: readReaction.emoji,
|
emoji: readReaction.emoji,
|
||||||
targetAuthorUuid: readReaction.targetAuthorUuid,
|
targetAuthorUuid: readReaction.targetAuthorUuid,
|
||||||
|
@ -99,7 +100,7 @@ export class ReadSyncs extends Collection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Notifications.removeBy({ messageId: found.id });
|
notificationService.removeBy({ messageId: found.id });
|
||||||
|
|
||||||
const message = window.MessageController.register(found.id, found);
|
const message = window.MessageController.register(found.id, found);
|
||||||
const readAt = Math.min(sync.get('readAt'), Date.now());
|
const readAt = Math.min(sync.get('readAt'), Date.now());
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { MessageModel } from '../models/messages';
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { markViewed } from '../services/MessageUpdater';
|
import { markViewed } from '../services/MessageUpdater';
|
||||||
import { isIncoming } from '../state/selectors/message';
|
import { isIncoming } from '../state/selectors/message';
|
||||||
|
import { notificationService } from '../services/notifications';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
type ViewSyncAttributesType = {
|
type ViewSyncAttributesType = {
|
||||||
|
@ -83,7 +84,7 @@ export class ViewSyncs extends Collection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Notifications.removeBy({ messageId: found.id });
|
notificationService.removeBy({ messageId: found.id });
|
||||||
|
|
||||||
const message = window.MessageController.register(found.id, found);
|
const message = window.MessageController.register(found.id, found);
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ import { migrateColor } from '../util/migrateColor';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { ourProfileKeyService } from '../services/ourProfileKey';
|
import { ourProfileKeyService } from '../services/ourProfileKey';
|
||||||
|
import { notificationService } from '../services/notifications';
|
||||||
import { getSendOptions } from '../util/getSendOptions';
|
import { getSendOptions } from '../util/getSendOptions';
|
||||||
import { isConversationAccepted } from '../util/isConversationAccepted';
|
import { isConversationAccepted } from '../util/isConversationAccepted';
|
||||||
import { markConversationRead } from '../util/markConversationRead';
|
import { markConversationRead } from '../util/markConversationRead';
|
||||||
|
@ -4847,7 +4848,7 @@ export class ConversationModel extends window.Backbone
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// As a performance optimization don't perform any work if notifications are
|
// As a performance optimization don't perform any work if notifications are
|
||||||
// disabled.
|
// disabled.
|
||||||
if (!window.Whisper.Notifications.isEnabled) {
|
if (!notificationService.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4900,7 +4901,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const messageId = message.id;
|
const messageId = message.id;
|
||||||
const isExpiringMessage = Message.hasExpiration(messageJSON);
|
const isExpiringMessage = Message.hasExpiration(messageJSON);
|
||||||
|
|
||||||
window.Whisper.Notifications.add({
|
notificationService.add({
|
||||||
senderTitle,
|
senderTitle,
|
||||||
conversationId,
|
conversationId,
|
||||||
notificationIconUrl,
|
notificationIconUrl,
|
||||||
|
|
|
@ -112,6 +112,7 @@ import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads';
|
||||||
import * as LinkPreview from '../types/LinkPreview';
|
import * as LinkPreview from '../types/LinkPreview';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
|
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
|
||||||
|
import { notificationService } from '../services/notifications';
|
||||||
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
|
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
@ -3292,7 +3293,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove any notifications for this message
|
// 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
|
// Erase the contents of this message
|
||||||
await this.eraseContents(
|
await this.eraseContents(
|
||||||
|
@ -3306,7 +3307,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearNotifications(reaction: Partial<ReactionType> = {}): void {
|
clearNotifications(reaction: Partial<ReactionType> = {}): void {
|
||||||
window.Whisper.Notifications.removeBy({
|
notificationService.removeBy({
|
||||||
...reaction,
|
...reaction,
|
||||||
messageId: this.id,
|
messageId: this.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { getStatus } from './getStatus';
|
|
||||||
|
|
||||||
export { getStatus };
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
|
||||||
|
import { notificationService } from './notifications';
|
||||||
|
|
||||||
function markReadOrViewed(
|
function markReadOrViewed(
|
||||||
messageAttrs: Readonly<MessageAttributesType>,
|
messageAttrs: Readonly<MessageAttributesType>,
|
||||||
|
@ -27,7 +28,7 @@ function markReadOrViewed(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Notifications.removeBy({ messageId });
|
notificationService.removeBy({ messageId });
|
||||||
|
|
||||||
if (!skipSave) {
|
if (!skipSave) {
|
||||||
window.Signal.Util.queueUpdateMessage(nextMessageAttributes);
|
window.Signal.Util.queueUpdateMessage(nextMessageAttributes);
|
||||||
|
|
|
@ -80,10 +80,10 @@ import {
|
||||||
REQUESTED_VIDEO_FRAMERATE,
|
REQUESTED_VIDEO_FRAMERATE,
|
||||||
} from '../calling/constants';
|
} from '../calling/constants';
|
||||||
import { callingMessageToProto } from '../util/callingMessageToProto';
|
import { callingMessageToProto } from '../util/callingMessageToProto';
|
||||||
import { notify } from './notify';
|
|
||||||
import { getSendOptions } from '../util/getSendOptions';
|
import { getSendOptions } from '../util/getSendOptions';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
|
import { notificationService } from './notifications';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -1117,7 +1117,7 @@ export class CallingClass {
|
||||||
|
|
||||||
if (source) {
|
if (source) {
|
||||||
ipcRenderer.send('show-screen-share', source.name);
|
ipcRenderer.send('show-screen-share', source.name);
|
||||||
notify({
|
notificationService.notify({
|
||||||
icon: 'images/icons/v2/video-solid-24.svg',
|
icon: 'images/icons/v2/video-solid-24.svg',
|
||||||
message: window.i18n('calling__presenting--notification-body'),
|
message: window.i18n('calling__presenting--notification-body'),
|
||||||
onNotificationClick: () => {
|
onNotificationClick: () => {
|
||||||
|
|
352
ts/services/notifications.ts
Normal file
352
ts/services/notifications.ts
Normal 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, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
|
@ -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, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -26,12 +26,16 @@ import {
|
||||||
SmartSafetyNumberViewer,
|
SmartSafetyNumberViewer,
|
||||||
Props as SafetyNumberViewerProps,
|
Props as SafetyNumberViewerProps,
|
||||||
} from './SafetyNumberViewer';
|
} from './SafetyNumberViewer';
|
||||||
import { notify } from '../../services/notify';
|
|
||||||
import { callingTones } from '../../util/callingTones';
|
import { callingTones } from '../../util/callingTones';
|
||||||
import {
|
import {
|
||||||
bounceAppIconStart,
|
bounceAppIconStart,
|
||||||
bounceAppIconStop,
|
bounceAppIconStop,
|
||||||
} from '../../shims/bounceAppIcon';
|
} from '../../shims/bounceAppIcon';
|
||||||
|
import {
|
||||||
|
FALLBACK_NOTIFICATION_TITLE,
|
||||||
|
NotificationSetting,
|
||||||
|
notificationService,
|
||||||
|
} from '../../services/notifications';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
|
||||||
function renderDeviceSelection(): JSX.Element {
|
function renderDeviceSelection(): JSX.Element {
|
||||||
|
@ -55,8 +59,27 @@ async function notifyForCall(
|
||||||
if (!shouldNotify) {
|
if (!shouldNotify) {
|
||||||
return;
|
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
|
icon: isVideoCall
|
||||||
? 'images/icons/v2/video-solid-24.svg'
|
? 'images/icons/v2/video-solid-24.svg'
|
||||||
: 'images/icons/v2/phone-right-solid-24.svg',
|
: 'images/icons/v2/phone-right-solid-24.svg',
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ConversationAttributesType } from '../model-types.d';
|
||||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||||
import { hasErrors } from '../state/selectors/message';
|
import { hasErrors } from '../state/selectors/message';
|
||||||
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
import { readSyncJobQueue } from '../jobs/readSyncJobQueue';
|
||||||
|
import { notificationService } from '../services/notifications';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
export async function markConversationRead(
|
export async function markConversationRead(
|
||||||
|
@ -39,7 +40,7 @@ export async function markConversationRead(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Notifications.removeBy({ conversationId });
|
notificationService.removeBy({ conversationId });
|
||||||
|
|
||||||
const unreadReactionSyncData = new Map<
|
const unreadReactionSyncData = new Map<
|
||||||
string,
|
string,
|
||||||
|
|
15
ts/window.d.ts
vendored
15
ts/window.d.ts
vendored
|
@ -176,6 +176,7 @@ declare global {
|
||||||
baseAttachmentsPath: string;
|
baseAttachmentsPath: string;
|
||||||
baseStickersPath: string;
|
baseStickersPath: string;
|
||||||
baseTempPath: string;
|
baseTempPath: string;
|
||||||
|
drawAttention: () => void;
|
||||||
enterKeyboardMode: () => void;
|
enterKeyboardMode: () => void;
|
||||||
enterMouseMode: () => void;
|
enterMouseMode: () => void;
|
||||||
getAccountManager: () => AccountManager;
|
getAccountManager: () => AccountManager;
|
||||||
|
@ -591,20 +592,6 @@ export type WhisperType = {
|
||||||
) => void;
|
) => 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: {
|
ExpiringMessagesListener: {
|
||||||
init: (events: Backbone.Events) => void;
|
init: (events: Backbone.Events) => void;
|
||||||
update: () => void;
|
update: () => void;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue