Files
Signal-Desktop/app/renderWindowsToast.std.tsx
2025-12-03 10:24:24 -08:00

101 lines
3.1 KiB
TypeScript

// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import {
type WindowsNotificationData,
NotificationType,
} from '../ts/types/notifications.std.js';
import { missingCaseError } from '../ts/util/missingCaseError.std.js';
import {
cancelPresentingRoute,
showConversationRoute,
showWindowRoute,
startCallLobbyRoute,
} from '../ts/util/signalRoutes.std.js';
function pathToUri(path: string) {
return `file:///${encodeURI(path.replace(/\\/g, '/'))}`;
}
function Toast(props: {
launch: string;
// Note: though React doesn't like it, Windows seems to require that this be camelcase
activationType: string;
children: React.ReactNode;
}) {
return React.createElement('toast', props);
}
function Visual(props: { children: React.ReactNode }) {
return React.createElement('visual', props);
}
function Binding(props: { template: string; children: React.ReactNode }) {
return React.createElement('binding', props);
}
function Text(props: { id: string; children: React.ReactNode }) {
return React.createElement('text', props);
}
function Image(props: { id: string; src: string; 'hint-crop': string }) {
return React.createElement('image', props);
}
function Audio(props: { src: string }) {
return React.createElement('audio', props);
}
export function renderWindowsToast({
avatarPath,
body,
heading,
token,
type,
}: WindowsNotificationData): string {
// Note: with these templates, the first <text> is one line, bolded
// https://learn.microsoft.com/en-us/previous-versions/windows/apps/hh761494(v=win.10)?redirectedfrom=MSDN#toastimageandtext02
// https://learn.microsoft.com/en-us/previous-versions/windows/apps/hh761494(v=win.10)?redirectedfrom=MSDN#toasttext02
const image = avatarPath ? (
<Image id="1" src={pathToUri(avatarPath)} hint-crop="circle" />
) : null;
const template = avatarPath ? 'ToastImageAndText02' : 'ToastText02';
let launch: URL;
let audio: React.ReactNode | undefined;
// Note:
// 1) this maps to the notify() function in services/notifications.ts
// 2) this also maps to the url-handling in main.ts
if (type === NotificationType.Message || type === NotificationType.Reaction) {
launch = showConversationRoute.toAppUrl({
token,
});
audio = <Audio src="ms-winsoundevent:Notification.IM" />;
} else if (type === NotificationType.IncomingGroupCall) {
launch = startCallLobbyRoute.toAppUrl({
token,
});
} else if (type === NotificationType.IncomingCall) {
launch = showWindowRoute.toAppUrl({});
} else if (type === NotificationType.IsPresenting) {
launch = cancelPresentingRoute.toAppUrl({});
} else if (type === NotificationType.MinimizedToTray) {
launch = showWindowRoute.toAppUrl({});
} else {
throw missingCaseError(type);
}
return renderToStaticMarkup(
<Toast launch={launch.href} activationType="protocol">
<Visual>
<Binding template={template}>
{image}
<Text id="1">{heading}</Text>
<Text id="2">{body}</Text>
</Binding>
</Visual>
{audio}
</Toast>
);
}