mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-10-01 09:22:22 +01:00
Save to PDF for recovery keys
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
co-authored by
Fedor Indutny
parent
9b875ea854
commit
21e267df43
@@ -8220,14 +8220,30 @@
|
||||
"messageformat": "2 keys",
|
||||
"description": "Description for Signal Login preferences item"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__Title": {
|
||||
"messageformat": "Your Signal Login",
|
||||
"description": "Title displayed in PDF version of Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__Description": {
|
||||
"messageformat": "Store this in a safe place, your login info is required to recover your account and data. Never share it with anyone.",
|
||||
"description": "Description displayed in PDF version of Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__AccountKey": {
|
||||
"messageformat": "Account key",
|
||||
"description": "Label for the Account Key in Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__AccountID": {
|
||||
"messageformat": "Account ID",
|
||||
"description": "Label for the Account ID in Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__RecoveryKey": {
|
||||
"messageformat": "Recovery key",
|
||||
"description": "Label for the Recovery Key in Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__SignalAccountKeys__Save": {
|
||||
"messageformat": "Save",
|
||||
"description": "Text of button in Signal Account Keys preferences page"
|
||||
},
|
||||
"icu:Preferences__Account__LinkedDevice__label": {
|
||||
"messageformat": "This is a linked device",
|
||||
"description": "Label of linked device section of Account settings"
|
||||
|
||||
+67
-1
@@ -29,7 +29,11 @@ import {
|
||||
safeStorage,
|
||||
protocol as electronProtocol,
|
||||
} from 'electron';
|
||||
import type { MenuItemConstructorOptions, Settings } from 'electron';
|
||||
import type {
|
||||
MenuItemConstructorOptions,
|
||||
Settings,
|
||||
BrowserWindowConstructorOptions,
|
||||
} from 'electron';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { packageJson } from '../ts/util/packageJson.main.ts';
|
||||
@@ -113,6 +117,7 @@ import { ChallengeMainHandler } from '../ts/main/challengeMain.main.ts';
|
||||
import { NativeThemeNotifier } from '../ts/main/NativeThemeNotifier.main.ts';
|
||||
import { PowerChannel } from '../ts/main/powerChannel.main.ts';
|
||||
import { SettingsChannel } from '../ts/main/settingsChannel.main.ts';
|
||||
import { PDFWindowPropsSchema } from '../ts/windows/pdf/types.std.ts';
|
||||
import '../ts/main/clipboardMain.main.ts';
|
||||
import { maybeParseUrl, setUrlSearchParams } from '../ts/util/url.std.ts';
|
||||
import { getHeicConverter } from '../ts/workers/heicConverterMain.main.ts';
|
||||
@@ -1138,6 +1143,67 @@ ipc.on('title-bar-double-click', () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipc.handle('pdf:generate', async (_event, data) => {
|
||||
const props = PDFWindowPropsSchema.parse(data);
|
||||
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
backgroundColor: '#ffffff',
|
||||
show: false,
|
||||
width: 612,
|
||||
height: 792,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
resizable: false,
|
||||
webPreferences: {
|
||||
...defaultWebPrefs,
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
preload: join(rootDir, 'bundles', 'preload', 'pdf.js'),
|
||||
},
|
||||
};
|
||||
|
||||
const pdfWindow = new BrowserWindow(options);
|
||||
|
||||
pdfWindow.webContents.ipc.on('pdf:getProps', async event => {
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = props;
|
||||
});
|
||||
|
||||
await handleCommonWindowEvents(pdfWindow);
|
||||
|
||||
const { promise, resolve, reject } =
|
||||
Promise.withResolvers<Uint8Array<ArrayBuffer>>();
|
||||
|
||||
try {
|
||||
pdfWindow.webContents.on('did-finish-load', async () => {
|
||||
try {
|
||||
const pdf = await pdfWindow.webContents.printToPDF({
|
||||
margins: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
preferCSSPageSize: true,
|
||||
printBackground: true,
|
||||
});
|
||||
pdfWindow.close();
|
||||
resolve(pdf as Uint8Array<ArrayBuffer>);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
await safeLoadURL(pdfWindow, prepareFileUrl([rootDir, 'pdf.html']));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
ipc.on('set-is-call-active', (_event, isCallActive) => {
|
||||
preventDisplaySleepService.setEnabled(isCallActive);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ const config = {
|
||||
'ts/windows/calldiagnostic/preload.preload.ts!',
|
||||
'ts/windows/debuglog/preload.preload.ts!',
|
||||
'ts/windows/loading/preload.preload.ts!',
|
||||
'ts/windows/pdf/preload.preload.ts!',
|
||||
'ts/windows/permissions/preload.preload.ts!',
|
||||
'ts/windows/screenShare/preload.preload.ts!',
|
||||
'ts/windows/sticker-creator/preload.preload.ts!',
|
||||
@@ -39,6 +40,7 @@ const config = {
|
||||
'ts/windows/calldiagnostic/app.dom.tsx!',
|
||||
'ts/windows/debuglog/app.dom.tsx!',
|
||||
'ts/windows/loading/start.dom.ts!',
|
||||
'ts/windows/pdf/app.dom.tsx!',
|
||||
'ts/windows/permissions/app.dom.tsx!',
|
||||
'ts/windows/screenShare/app.dom.tsx!',
|
||||
// Others
|
||||
|
||||
@@ -45,6 +45,7 @@ const sandboxPreload = {
|
||||
'preload/about': 'ts/windows/about/preload.preload.ts',
|
||||
'preload/calldiagnostic': 'ts/windows/calldiagnostic/preload.preload.ts',
|
||||
'preload/debuglog': 'ts/windows/debuglog/preload.preload.ts',
|
||||
'preload/pdf': 'ts/windows/pdf/preload.preload.ts',
|
||||
'preload/permissions': 'ts/windows/permissions/preload.preload.ts',
|
||||
'preload/screenShare': 'ts/windows/screenShare/preload.preload.ts',
|
||||
'preload/sticker-creator': 'ts/windows/sticker-creator/preload.preload.ts',
|
||||
@@ -55,6 +56,7 @@ const sandboxDOM = {
|
||||
'dom/calldiagnostic': 'ts/windows/calldiagnostic/app.dom.tsx',
|
||||
'dom/debuglog': 'ts/windows/debuglog/app.dom.tsx',
|
||||
'dom/loading': 'ts/windows/loading/start.dom.ts',
|
||||
'dom/pdf': 'ts/windows/pdf/app.dom.tsx',
|
||||
'dom/permissions': 'ts/windows/permissions/app.dom.tsx',
|
||||
'dom/screenShare': 'ts/windows/screenShare/app.dom.tsx',
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
sans-serif;
|
||||
/* Note: This font-family is checked for in matchMonospace, to support paste scenarios */
|
||||
--font-mono:
|
||||
'MonoSpecial',
|
||||
'SF Mono',
|
||||
SFMono-Regular,
|
||||
ui-monospace,
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
@apply h-screen scheme-light-dark select-none;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: 612px 792px;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply h-screen w-screen bg-surface-primary font-sans type-body-large text-primary;
|
||||
@apply h-screen w-screen bg-surface-primary print:bg-white font-sans type-body-large text-primary;
|
||||
}
|
||||
|
||||
a[href] {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util.std.ts';
|
||||
import type { PDFWindowPropsType } from '../windows/pdf/types.std.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import { SignalAccountKeys } from './preferences/SignalAccountKeys.dom.tsx';
|
||||
|
||||
export type PDFProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
}> &
|
||||
PDFWindowPropsType;
|
||||
|
||||
export function Pdf(props: PDFProps): JSX.Element {
|
||||
const { i18n } = props;
|
||||
|
||||
if (props.view === 'account-keys') {
|
||||
return (
|
||||
<SignalAccountKeys
|
||||
i18n={i18n}
|
||||
serviceId={props.serviceId}
|
||||
backupKey={props.backupKey}
|
||||
saveAccountKeysPDF={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
throw missingCaseError(props.view);
|
||||
}
|
||||
@@ -718,6 +718,9 @@ export default {
|
||||
setGroupSvcModeForScreenshare: action('setGroupSvcModeForScreenshare'),
|
||||
sfuUrl: 'https://sfu.voip.signal.org',
|
||||
setSfuUrl: action('setSfuUrl'),
|
||||
saveAccountKeysPDF: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
forceKeyTransparencyCheck: async () => {
|
||||
await sleep(1000);
|
||||
},
|
||||
|
||||
@@ -408,6 +408,7 @@ type PropsFunctionType = {
|
||||
setGroupSvcMode: (value: string | undefined) => void;
|
||||
setGroupSvcModeForScreenshare: (value: string | undefined) => void;
|
||||
setSfuUrl: (value: string | undefined) => void;
|
||||
saveAccountKeysPDF: () => Promise<void>;
|
||||
forceKeyTransparencyCheck: () => Promise<void>;
|
||||
keyTransparencySelfHealth: StorageAccessType['keyTransparencySelfHealth'];
|
||||
|
||||
@@ -665,6 +666,7 @@ export function Preferences({
|
||||
setGroupSvcModeForScreenshare,
|
||||
setSfuUrl,
|
||||
sfuUrl,
|
||||
saveAccountKeysPDF,
|
||||
forceKeyTransparencyCheck,
|
||||
keyTransparencySelfHealth,
|
||||
weArePrimaryDevice,
|
||||
@@ -962,6 +964,7 @@ export function Preferences({
|
||||
i18n={i18n}
|
||||
serviceId={me.serviceId}
|
||||
backupKey={backupKey}
|
||||
saveAccountKeysPDF={saveAccountKeysPDF}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { JSX } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { SignalAccountKeys } from './SignalAccountKeys.dom.tsx';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/Preferences/SignalAccountKeys',
|
||||
} satisfies Meta;
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return (
|
||||
<SignalAccountKeys
|
||||
i18n={i18n}
|
||||
serviceId="f83a157c-aed6-41ff-9d84-637c7af95f02"
|
||||
backupKey="22rsumdlqxa8r7lsghj12ah6ae2anhk4vjvccor4pc75mee49i9t9dcwmvso4wyn"
|
||||
saveAccountKeysPDF={() => Promise.resolve()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Print(): JSX.Element {
|
||||
return (
|
||||
<SignalAccountKeys
|
||||
i18n={i18n}
|
||||
serviceId="f83a157c-aed6-41ff-9d84-637c7af95f02"
|
||||
backupKey="22rsumdlqxa8r7lsghj12ah6ae2anhk4vjvccor4pc75mee49i9t9dcwmvso4wyn"
|
||||
saveAccountKeysPDF={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { AxoButton } from '../../axo/AxoButton.dom.tsx';
|
||||
import type { LocalizerType } from '../../types/Util.std.ts';
|
||||
import { formatBackupKeyForDisplay } from '../../util/formatBackupKeyForDisplay.std.ts';
|
||||
|
||||
@@ -10,56 +12,134 @@ type SignalAccountKeysPropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
serviceId: string | undefined;
|
||||
backupKey: string | undefined;
|
||||
saveAccountKeysPDF: (() => Promise<void>) | undefined;
|
||||
}>;
|
||||
|
||||
export function SignalAccountKeys({
|
||||
i18n,
|
||||
serviceId,
|
||||
backupKey,
|
||||
saveAccountKeysPDF: doSaveAccountKeysPDF,
|
||||
}: SignalAccountKeysPropsType): ReactNode {
|
||||
const backupKeyForDisplay =
|
||||
backupKey &&
|
||||
formatBackupKeyForDisplay(backupKey, {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const backupKeyForDisplay = useMemo(() => {
|
||||
if (!backupKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const display = formatBackupKeyForDisplay(backupKey, {
|
||||
convertAmbiguousChars: true,
|
||||
});
|
||||
|
||||
const print = display.split(/\s+/g).map((part, i) => {
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
return <span key={`backup-part-${i}`}>{part}</span>;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={tw('print:hidden')}>{display}</span>
|
||||
<span
|
||||
className={tw(
|
||||
'hidden grid-cols-4 gap-x-13 print:grid print:leading-6'
|
||||
)}
|
||||
>
|
||||
{print}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}, [backupKey]);
|
||||
|
||||
const saveAccountKeysPDF = useCallback(async () => {
|
||||
if (!doSaveAccountKeysPDF) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await doSaveAccountKeysPDF();
|
||||
} catch {
|
||||
// Ignore
|
||||
// oxlint-disable-next-line react/todo
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [doSaveAccountKeysPDF]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
alt={i18n('icu:Preferences__SignalAccount__label')}
|
||||
className={tw('mx-auto mb-6.5')}
|
||||
src="images/signal-login.svg"
|
||||
width="98"
|
||||
height="56"
|
||||
/>
|
||||
<div
|
||||
className={tw(
|
||||
'flex flex-col gap-4 print:mx-auto print:max-w-97.5 print:gap-8 print:pbs-30'
|
||||
)}
|
||||
>
|
||||
<div className={tw('text-center')}>
|
||||
<img
|
||||
alt={i18n('icu:Preferences__SignalAccount__label')}
|
||||
className={tw('mx-auto mb-2.5 print:mb-4')}
|
||||
src="images/signal-login.svg"
|
||||
width="98"
|
||||
height="56"
|
||||
/>
|
||||
|
||||
<div className={tw('hidden text-center print:block')}>
|
||||
<h1 className={tw('mb-2 type-title-medium')}>
|
||||
{i18n('icu:Preferences__SignalAccountKeys__Title')}
|
||||
</h1>
|
||||
{/* oxlint-disable-next-line better-tailwindcss/no-restricted-classes */}
|
||||
<div className={tw('type-body-large text-[#4D4D4D]')}>
|
||||
{i18n('icu:Preferences__SignalAccountKeys__Description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{serviceId && (
|
||||
<KeySection
|
||||
label={i18n('icu:Preferences__SignalAccountKeys__AccountKey')}
|
||||
value={serviceId}
|
||||
/>
|
||||
label={i18n('icu:Preferences__SignalAccountKeys__AccountID')}
|
||||
>
|
||||
{serviceId.toUpperCase()}
|
||||
</KeySection>
|
||||
)}
|
||||
<div className={tw('mb-4')} />
|
||||
{backupKeyForDisplay && (
|
||||
<KeySection
|
||||
label={i18n('icu:Preferences__SignalAccountKeys__RecoveryKey')}
|
||||
value={backupKeyForDisplay}
|
||||
/>
|
||||
>
|
||||
{backupKeyForDisplay}
|
||||
</KeySection>
|
||||
)}
|
||||
</>
|
||||
|
||||
{doSaveAccountKeysPDF && (
|
||||
<div className={tw('mx-auto')}>
|
||||
<AxoButton.Root
|
||||
symbol="download"
|
||||
variant="subtle-secondary"
|
||||
pending={isSaving}
|
||||
size="md"
|
||||
onClick={saveAccountKeysPDF}
|
||||
>
|
||||
{i18n('icu:Preferences__SignalAccountKeys__Save')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeySection({
|
||||
label,
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<section>
|
||||
<h2 className={tw('px-4 py-2 type-body-medium font-semibold')}>
|
||||
<h2
|
||||
className={tw(
|
||||
'px-4 py-2 type-body-medium font-semibold',
|
||||
'print:px-6 print:py-3 print:type-title-small'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</h2>
|
||||
<div
|
||||
@@ -69,10 +149,22 @@ function KeySection({
|
||||
'curved-2xl bg-surface-card p-1 shadow-elevation-0',
|
||||
'forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder]',
|
||||
'w-full px-4 py-2.5'
|
||||
'w-full px-4 py-2.5',
|
||||
|
||||
'print:px-5 print:py-4',
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
'print:bg-[#F4F4F5] print:shadow-none',
|
||||
'print:flex print:justify-center'
|
||||
)}
|
||||
>
|
||||
<div className={tw('w-full max-w-[40ch] font-mono')}>{value}</div>
|
||||
<div
|
||||
className={tw(
|
||||
'w-full max-w-[40ch] font-mono tracking-[1.12px]',
|
||||
'print:max-w-fit print:type-body-large print:tracking-[1.12px]'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -996,6 +996,18 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
drop(itemStorage.put('sfuUrl', value));
|
||||
}, []);
|
||||
|
||||
const saveAccountKeysPDF = useCallback(async () => {
|
||||
const data = await window.Events.generatePDF({
|
||||
view: 'account-keys',
|
||||
serviceId: me.serviceId,
|
||||
backupKey,
|
||||
});
|
||||
await saveAttachmentToDisk({
|
||||
name: 'Signal_AccountKeys.pdf',
|
||||
data,
|
||||
});
|
||||
}, [me.serviceId, backupKey]);
|
||||
|
||||
if (currentLocation.tab !== NavTab.Settings) {
|
||||
return null;
|
||||
}
|
||||
@@ -1246,6 +1258,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
sfuUrl={items.sfuUrl}
|
||||
setSfuUrl={setSfuUrl}
|
||||
forceKeyTransparencyCheck={forceKeyTransparencyCheck}
|
||||
saveAccountKeysPDF={saveAccountKeysPDF}
|
||||
keyTransparencySelfHealth={items.keyTransparencySelfHealth}
|
||||
weArePrimaryDevice={weArePrimaryDevice}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
} from './preload.preload.ts';
|
||||
import { SystemTraySetting } from '../types/SystemTraySetting.std.ts';
|
||||
import { putStickers } from '../textsecure/WebAPI.preload.ts';
|
||||
import type { PDFWindowPropsType } from '../windows/pdf/types.std.ts';
|
||||
import OS from './os/osPreload.preload.ts';
|
||||
|
||||
const { noop } = lodash;
|
||||
@@ -60,6 +61,7 @@ export type IPCEventsCallbacksType = {
|
||||
mediaType: 'screen' | 'microphone' | 'camera'
|
||||
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
|
||||
installStickerPack: (packId: string, key: string) => Promise<void>;
|
||||
generatePDF: (args: PDFWindowPropsType) => Promise<Uint8Array<ArrayBuffer>>;
|
||||
requestCloseConfirmation: () => Promise<boolean>;
|
||||
setMediaPlaybackDisabled: (playbackDisabled: boolean) => void;
|
||||
showConversationViaNotification: (data: NotificationClickData) => void;
|
||||
@@ -274,6 +276,9 @@ export function createIPCEvents(
|
||||
actionSource: 'ui',
|
||||
});
|
||||
},
|
||||
generatePDF: (args: PDFWindowPropsType) => {
|
||||
return ipcRenderer.invoke('pdf:generate', args);
|
||||
},
|
||||
requestCloseConfirmation: async (): Promise<boolean> => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
||||
Vendored
+2
@@ -26,6 +26,7 @@ import type { WindowsNotificationData } from './services/notifications.preload.t
|
||||
import type { QueryStatsOptions } from './sql/main.main.ts';
|
||||
import type { SocketStatuses } from './textsecure/SocketManager.preload.ts';
|
||||
import type { SignalClipboardType } from './windows/clipboard.preload.ts';
|
||||
import type { PDFWindowPropsType } from './windows/pdf/types.std.ts';
|
||||
|
||||
export type IPCType = {
|
||||
addSetupMenuItems: () => void;
|
||||
@@ -124,6 +125,7 @@ type SettingsWindowPropsType = {
|
||||
|
||||
export type SignalCoreType = {
|
||||
AboutWindowProps?: AboutWindowPropsType;
|
||||
PDFWindowProps?: PDFWindowPropsType;
|
||||
CallDiagnosticWindowProps?: CallDiagnosticWindowPropsType;
|
||||
DebugLogWindowProps?: DebugLogWindowPropsType;
|
||||
PermissionsWindowProps?: PermissionsWindowPropsType;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import '../sandboxedInit.dom.ts';
|
||||
import { Pdf } from '../../components/Pdf.dom.tsx';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { AppProvider } from '../AppProvider.dom.tsx';
|
||||
import { AxoTheme } from '../../axo/AxoTheme.dom.tsx';
|
||||
import { tw } from '../../axo/tw.dom.tsx';
|
||||
import { setDocumentLocale } from '../../util/setDocumentLocale.dom.ts';
|
||||
|
||||
const { PDFWindowProps } = window.Signal;
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
setDocumentLocale(document);
|
||||
|
||||
strictAssert(PDFWindowProps, 'window values not provided');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
strictAssert(app != null, 'No #app');
|
||||
|
||||
createRoot(app).render(
|
||||
<AppProvider>
|
||||
<AxoTheme.Override theme="force-light">
|
||||
<div className={tw('text-primary')}>
|
||||
<Pdf i18n={i18n} {...PDFWindowProps} />
|
||||
</div>
|
||||
</AxoTheme.Override>
|
||||
</AppProvider>
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import { MinimalSignalContext } from '../minimalContext.preload.ts';
|
||||
import { PDFWindowPropsSchema } from './types.std.ts';
|
||||
|
||||
const Signal = {
|
||||
PDFWindowProps: PDFWindowPropsSchema.parse(
|
||||
ipcRenderer.sendSync('pdf:getProps')
|
||||
),
|
||||
};
|
||||
contextBridge.exposeInMainWorld('Signal', Signal);
|
||||
contextBridge.exposeInMainWorld('SignalContext', MinimalSignalContext);
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as z from 'zod/mini';
|
||||
|
||||
const PDFAccountKeysPropsSchema = z.object({
|
||||
view: z.literal('account-keys'),
|
||||
serviceId: z.union([z.string(), z.undefined()]),
|
||||
backupKey: z.union([z.string(), z.undefined()]),
|
||||
});
|
||||
|
||||
export const PDFWindowPropsSchema = z.union([PDFAccountKeysPropsSchema]);
|
||||
|
||||
export type PDFWindowPropsType = z.infer<typeof PDFWindowPropsSchema>;
|
||||
Reference in New Issue
Block a user