mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Interact with Windows notifications from a thread
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
@@ -1,55 +1,78 @@
|
||||
// Copyright 2017 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ipcMain as ipc } from 'electron';
|
||||
import { ipcMain as ipc, app } from 'electron';
|
||||
import type { IpcMainInvokeEvent } from 'electron';
|
||||
import { join } from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import {
|
||||
Notifier,
|
||||
sendDummyKeystroke,
|
||||
} from '@indutny/simple-windows-notifications';
|
||||
|
||||
import { createLogger } from '../ts/logging/log.std.js';
|
||||
import { AUMID } from './startup_config.main.js';
|
||||
import type { WindowsNotificationData } from '../ts/services/notifications.preload.js';
|
||||
import type {
|
||||
WindowsNotificationWorkerDataType,
|
||||
WindowsNotificationRequestType,
|
||||
WindowsNotificationData,
|
||||
} from '../ts/types/notifications.std.js';
|
||||
import { WindowsNotificationDataSchema } from '../ts/types/notifications.std.js';
|
||||
import OS from '../ts/util/os/osMain.node.js';
|
||||
import { renderWindowsToast } from './renderWindowsToast.std.js';
|
||||
|
||||
export { sendDummyKeystroke };
|
||||
import { createLogger } from '../ts/logging/log.std.js';
|
||||
|
||||
const log = createLogger('WindowsNotifications');
|
||||
|
||||
if (OS.isWindows()) {
|
||||
const notifier = new Notifier(AUMID);
|
||||
let worker: Worker | undefined;
|
||||
|
||||
const NOTIFICATION_ID = {
|
||||
group: 'group',
|
||||
tag: 'tag',
|
||||
};
|
||||
if (OS.isWindows()) {
|
||||
const scriptPath = join(
|
||||
app.getAppPath(),
|
||||
'app',
|
||||
'WindowsNotificationsWorker.node.js'
|
||||
);
|
||||
|
||||
worker = new Worker(scriptPath, {
|
||||
workerData: {
|
||||
AUMID,
|
||||
} satisfies WindowsNotificationWorkerDataType,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendDummyKeystroke(): void {
|
||||
if (worker == null) {
|
||||
log.warn('sendDummyKeystroke without worker');
|
||||
return;
|
||||
}
|
||||
worker.postMessage({
|
||||
command: 'sendDummyKeystroke',
|
||||
} satisfies WindowsNotificationRequestType);
|
||||
}
|
||||
|
||||
export function show(notificationData: WindowsNotificationData): void {
|
||||
if (worker == null) {
|
||||
log.warn('show without worker');
|
||||
return;
|
||||
}
|
||||
worker.postMessage({
|
||||
command: 'show',
|
||||
notificationData,
|
||||
} satisfies WindowsNotificationRequestType);
|
||||
}
|
||||
|
||||
ipc.handle(
|
||||
'windows-notifications:show',
|
||||
(_event: IpcMainInvokeEvent, data: WindowsNotificationData) => {
|
||||
(_event: IpcMainInvokeEvent, data: unknown) => {
|
||||
try {
|
||||
// First, clear all previous notifications - we want just one
|
||||
// notification at a time
|
||||
notifier.remove(NOTIFICATION_ID);
|
||||
notifier.show(renderWindowsToast(data), NOTIFICATION_ID);
|
||||
const notificationData = WindowsNotificationDataSchema.parse(data);
|
||||
show(notificationData);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Windows Notifications: Failed to show notification: ${error.stack}`
|
||||
);
|
||||
log.error('failed to parse notification data', error.stack);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipc.handle('windows-notifications:clear-all', () => {
|
||||
try {
|
||||
notifier.remove(NOTIFICATION_ID);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Windows Notifications: Failed to clear notifications: ${error.stack}`
|
||||
);
|
||||
if (worker == null) {
|
||||
log.warn('clear all without worker');
|
||||
return;
|
||||
}
|
||||
worker.postMessage({
|
||||
command: 'clearAll',
|
||||
} satisfies WindowsNotificationRequestType);
|
||||
});
|
||||
}
|
||||
|
||||
83
app/WindowsNotificationsWorker.node.ts
Normal file
83
app/WindowsNotificationsWorker.node.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
|
||||
import {
|
||||
Notifier,
|
||||
sendDummyKeystroke,
|
||||
} from '@indutny/simple-windows-notifications';
|
||||
|
||||
import { createLogger } from '../ts/logging/log.std.js';
|
||||
import {
|
||||
WindowsNotificationWorkerDataSchema,
|
||||
WindowsNotificationRequestSchema,
|
||||
} from '../ts/types/notifications.std.js';
|
||||
import OS from '../ts/util/os/osMain.node.js';
|
||||
import { missingCaseError } from '../ts/util/missingCaseError.std.js';
|
||||
import { renderWindowsToast } from './renderWindowsToast.std.js';
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error('Must run as a worker thread');
|
||||
}
|
||||
|
||||
if (!OS.isWindows()) {
|
||||
throw new Error('Runs only on Windows');
|
||||
}
|
||||
|
||||
const port = parentPort;
|
||||
|
||||
const log = createLogger('WindowsNotificationsWorker');
|
||||
|
||||
const { AUMID } = WindowsNotificationWorkerDataSchema.parse(workerData);
|
||||
const notifier = new Notifier(AUMID);
|
||||
|
||||
const NOTIFICATION_ID = {
|
||||
group: 'group',
|
||||
tag: 'tag',
|
||||
};
|
||||
|
||||
port.on('message', (message: unknown) => {
|
||||
const request = WindowsNotificationRequestSchema.parse(message);
|
||||
|
||||
if (request.command === 'show') {
|
||||
try {
|
||||
// First, clear all previous notifications - we want just one
|
||||
// notification at a time
|
||||
notifier.remove(NOTIFICATION_ID);
|
||||
notifier.show(
|
||||
renderWindowsToast(request.notificationData),
|
||||
NOTIFICATION_ID
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Windows Notifications: Failed to show notification: ${error.stack}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.command === 'clearAll') {
|
||||
try {
|
||||
notifier.remove(NOTIFICATION_ID);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Windows Notifications: Failed to clear notifications: ${error.stack}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.command === 'sendDummyKeystroke') {
|
||||
try {
|
||||
sendDummyKeystroke();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Windows Notifications: Failed to send dummy keystroke: ${error.stack}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
throw missingCaseError(request);
|
||||
});
|
||||
@@ -47,6 +47,7 @@ import { strictAssert } from '../ts/util/assert.std.js';
|
||||
import { drop } from '../ts/util/drop.std.js';
|
||||
import type { ThemeSettingType } from '../ts/types/StorageUIKeys.std.js';
|
||||
import { ThemeType } from '../ts/types/Util.std.js';
|
||||
import { NotificationType } from '../ts/types/notifications.std.js';
|
||||
import * as Errors from '../ts/types/errors.std.js';
|
||||
import { resolveCanonicalLocales } from '../ts/util/resolveCanonicalLocales.std.js';
|
||||
import { createLogger } from '../ts/logging/log.std.js';
|
||||
@@ -136,7 +137,10 @@ import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas.std.js';
|
||||
import { getAppErrorIcon } from '../ts/util/getAppErrorIcon.node.js';
|
||||
import { promptOSAuth } from '../ts/util/os/promptOSAuthMain.main.js';
|
||||
import { appRelaunch } from '../ts/util/relaunch.main.js';
|
||||
import { sendDummyKeystroke } from './WindowsNotifications.main.js';
|
||||
import {
|
||||
sendDummyKeystroke,
|
||||
show as showWindowsNotification,
|
||||
} from './WindowsNotifications.main.js';
|
||||
|
||||
const { chmod, realpath, writeFile } = fsExtra;
|
||||
const { get, pick, isNumber, isBoolean, some, debounce, noop } = lodash;
|
||||
@@ -926,7 +930,10 @@ async function createWindow() {
|
||||
!windowState.shouldQuit() &&
|
||||
(usingTrayIcon || OS.isMacOS())
|
||||
) {
|
||||
if (usingTrayIcon) {
|
||||
if (!usingTrayIcon) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shownTrayNotice = ephemeralConfig.get('shown-tray-notice');
|
||||
if (shownTrayNotice) {
|
||||
log.info('close: not showing tray notice');
|
||||
@@ -936,6 +943,20 @@ async function createWindow() {
|
||||
ephemeralConfig.set('shown-tray-notice', true);
|
||||
log.info('close: showing tray notice');
|
||||
|
||||
if (OS.isWindows()) {
|
||||
showWindowsNotification({
|
||||
type: NotificationType.MinimizedToTray,
|
||||
token: 'unused',
|
||||
heading: getResolvedMessagesLocale().i18n(
|
||||
'icu:minimizeToTrayNotification--title'
|
||||
),
|
||||
body: getResolvedMessagesLocale().i18n(
|
||||
'icu:minimizeToTrayNotification--body'
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const n = new Notification({
|
||||
title: getResolvedMessagesLocale().i18n(
|
||||
'icu:minimizeToTrayNotification--title'
|
||||
@@ -946,7 +967,6 @@ async function createWindow() {
|
||||
});
|
||||
|
||||
n.show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
import type { WindowsNotificationData } from '../ts/services/notifications.preload.js';
|
||||
|
||||
import { NotificationType } from '../ts/types/notifications.std.js';
|
||||
import {
|
||||
type WindowsNotificationData,
|
||||
NotificationType,
|
||||
} from '../ts/types/notifications.std.js';
|
||||
import { missingCaseError } from '../ts/util/missingCaseError.std.js';
|
||||
import {
|
||||
cancelPresentingRoute,
|
||||
@@ -78,6 +79,8 @@ export function renderWindowsToast({
|
||||
launch = showWindowRoute.toAppUrl({});
|
||||
} else if (type === NotificationType.IsPresenting) {
|
||||
launch = cancelPresentingRoute.toAppUrl({});
|
||||
} else if (type === NotificationType.MinimizedToTray) {
|
||||
launch = showWindowRoute.toAppUrl({});
|
||||
} else {
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
@@ -53,13 +53,6 @@ export type NotificationClickData = Readonly<{
|
||||
messageId: string | undefined;
|
||||
storyId: string | undefined;
|
||||
}>;
|
||||
export type WindowsNotificationData = {
|
||||
avatarPath?: string;
|
||||
body: string;
|
||||
heading: string;
|
||||
token: string;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
// 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.
|
||||
@@ -230,6 +223,9 @@ class NotificationService extends EventEmitter {
|
||||
window.reduxActions?.calling?.cancelPresenting();
|
||||
} else if (type === NotificationType.IncomingCall) {
|
||||
window.IPC.showWindow();
|
||||
} else if (type === NotificationType.MinimizedToTray) {
|
||||
// Unused since the notification is shown by main process instead
|
||||
window.IPC.showWindow();
|
||||
} else {
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,50 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import z from 'zod';
|
||||
|
||||
export enum NotificationType {
|
||||
IncomingCall = 'IncomingCall',
|
||||
IncomingGroupCall = 'IncomingGroupCall',
|
||||
IsPresenting = 'IsPresenting',
|
||||
Message = 'Message',
|
||||
Reaction = 'Reaction',
|
||||
MinimizedToTray = 'MinimizedToTray',
|
||||
}
|
||||
|
||||
export const WindowsNotificationDataSchema = z.object({
|
||||
avatarPath: z.string().optional(),
|
||||
body: z.string(),
|
||||
heading: z.string(),
|
||||
token: z.string(),
|
||||
type: z.nativeEnum(NotificationType),
|
||||
});
|
||||
|
||||
export type WindowsNotificationData = z.infer<
|
||||
typeof WindowsNotificationDataSchema
|
||||
>;
|
||||
|
||||
export const WindowsNotificationWorkerDataSchema = z.object({
|
||||
AUMID: z.string(),
|
||||
});
|
||||
|
||||
export type WindowsNotificationWorkerDataType = z.infer<
|
||||
typeof WindowsNotificationWorkerDataSchema
|
||||
>;
|
||||
|
||||
export const WindowsNotificationRequestSchema = z.union([
|
||||
z.object({
|
||||
command: z.literal('show'),
|
||||
notificationData: WindowsNotificationDataSchema,
|
||||
}),
|
||||
z.object({
|
||||
command: z.literal('clearAll'),
|
||||
}),
|
||||
z.object({
|
||||
command: z.literal('sendDummyKeystroke'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type WindowsNotificationRequestType = z.infer<
|
||||
typeof WindowsNotificationRequestSchema
|
||||
>;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { strictAssert } from '../../util/assert.std.js';
|
||||
import { drop } from '../../util/drop.std.js';
|
||||
import { explodePromise } from '../../util/explodePromise.std.js';
|
||||
import { DataReader } from '../../sql/Client.preload.js';
|
||||
import type { WindowsNotificationData } from '../../services/notifications.preload.js';
|
||||
import type { WindowsNotificationData } from '../../types/notifications.std.js';
|
||||
import { finish3dsValidation } from '../../services/donations.preload.js';
|
||||
import { AggregatedStats } from '../../textsecure/WebsocketResources.preload.js';
|
||||
import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager.preload.js';
|
||||
|
||||
Reference in New Issue
Block a user