mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Add backup validation to settings
This commit is contained in:
@@ -6600,6 +6600,18 @@
|
||||
"messageformat": "Backups",
|
||||
"description": "Button to switch the settings view to control message & media backups"
|
||||
},
|
||||
"icu:Preferences__button--internal": {
|
||||
"messageformat": "Internal",
|
||||
"description": "Button to switch the settings view to control internal configuration"
|
||||
},
|
||||
"icu:Preferences__internal__validate-backup--description": {
|
||||
"messageformat": "Export encrypted backup to memory and run validation suite on it",
|
||||
"description": "Description of the internal backup validation tool"
|
||||
},
|
||||
"icu:Preferences__internal__validate-backup": {
|
||||
"messageformat": "Validate",
|
||||
"description": "Button to run internal backup validation tool"
|
||||
},
|
||||
"icu:Preferences--lastSynced": {
|
||||
"messageformat": "Last import at {date} {time}",
|
||||
"description": "Label for date and time of last sync operation"
|
||||
|
||||
1
images/icons/v3/internal/internal.svg
Normal file
1
images/icons/v3/internal/internal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 5.938a4.063 4.063 0 1 0 0 8.125 4.063 4.063 0 0 0 0-8.126ZM7.396 10a2.604 2.604 0 1 1 5.208 0 2.604 2.604 0 0 1-5.208 0Z" fill="#000"/><path d="M.98 9.01a1.98 1.98 0 0 0 0 1.98l3.653 6.327a1.98 1.98 0 0 0 1.714.99h7.306c.707 0 1.36-.377 1.714-.99l3.653-6.327a1.979 1.979 0 0 0 0-1.98l-3.653-6.327a1.98 1.98 0 0 0-1.714-.99H6.347a1.98 1.98 0 0 0-1.714.99L.979 9.01Zm1.262 1.25a.52.52 0 0 1 0-.52l3.654-6.328a.52.52 0 0 1 .45-.26h7.307a.52.52 0 0 1 .451.26l3.654 6.328a.521.521 0 0 1 0 .52l-3.654 6.328a.52.52 0 0 1-.45.26H6.346a.52.52 0 0 1-.451-.26L2.242 10.26Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 670 B |
@@ -120,6 +120,10 @@ $secondary-text-color: light-dark(
|
||||
&--backups {
|
||||
@include preferences-icon('../images/icons/v3/backup/backup-bold.svg');
|
||||
}
|
||||
|
||||
&--internal {
|
||||
@include preferences-icon('../images/icons/v3/internal/internal.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&__settings-pane {
|
||||
@@ -539,3 +543,20 @@ $secondary-text-color: light-dark(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Preferences--internal--validate-backup--result {
|
||||
padding-inline: 48px 24px;
|
||||
}
|
||||
|
||||
.Preferences--internal--validate-backup--error {
|
||||
padding-inline: 48px 24px;
|
||||
color: variables.$color-accent-red;
|
||||
}
|
||||
|
||||
.Preferences--internal--validate-backup--result pre,
|
||||
.Preferences--internal--validate-backup--error pre {
|
||||
max-height: 128px;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ export default {
|
||||
isNotificationAttentionSupported: true,
|
||||
isSyncSupported: true,
|
||||
isSystemTraySupported: true,
|
||||
isInternalUser: false,
|
||||
isMinimizeToAndStartInSystemTraySupported: true,
|
||||
lastSyncTime: Date.now(),
|
||||
localeOverride: null,
|
||||
@@ -189,6 +190,22 @@ export default {
|
||||
setGlobalDefaultConversationColor: action(
|
||||
'setGlobalDefaultConversationColor'
|
||||
),
|
||||
validateBackup: async () => {
|
||||
return {
|
||||
totalBytes: 100,
|
||||
stats: {
|
||||
adHocCalls: 1,
|
||||
callLinks: 2,
|
||||
conversations: 3,
|
||||
chats: 4,
|
||||
distributionLists: 5,
|
||||
messages: 6,
|
||||
skippedMessages: 7,
|
||||
stickerPacks: 8,
|
||||
fixedDirectMessages: 9,
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies PropsType,
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
@@ -300,3 +317,9 @@ BackupsSubscriptionExpired.args = {
|
||||
status: 'expired',
|
||||
},
|
||||
};
|
||||
|
||||
export const Internal = Template.bind({});
|
||||
Internal.args = {
|
||||
initialPage: Page.Internal,
|
||||
isInternalUser: true,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import classNames from 'classnames';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
import type { MediaDeviceSettings } from '../types/Calling';
|
||||
import type { ExportResultType as BackupExportResultType } from '../services/backups';
|
||||
import type {
|
||||
AutoDownloadAttachmentType,
|
||||
NotificationSettingType,
|
||||
@@ -75,6 +76,7 @@ import {
|
||||
SettingsRow,
|
||||
} from './PreferencesUtil';
|
||||
import { PreferencesBackups } from './PreferencesBackups';
|
||||
import { PreferencesInternal } from './PreferencesInternal';
|
||||
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
@@ -145,6 +147,7 @@ export type PropsDataType = {
|
||||
isSyncSupported: boolean;
|
||||
isSystemTraySupported: boolean;
|
||||
isMinimizeToAndStartInSystemTraySupported: boolean;
|
||||
isInternalUser: boolean;
|
||||
|
||||
availableCameras: Array<
|
||||
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
|
||||
@@ -175,6 +178,7 @@ type PropsFunctionType = {
|
||||
value: CustomColorType;
|
||||
}
|
||||
) => unknown;
|
||||
validateBackup: () => Promise<BackupExportResultType>;
|
||||
|
||||
// Change handlers
|
||||
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
||||
@@ -232,6 +236,7 @@ export enum Page {
|
||||
Privacy = 'Privacy',
|
||||
DataUsage = 'DataUsage',
|
||||
Backups = 'Backups',
|
||||
Internal = 'Internal',
|
||||
|
||||
// Sub pages
|
||||
ChatColor = 'ChatColor',
|
||||
@@ -319,6 +324,7 @@ export function Preferences({
|
||||
isSyncSupported,
|
||||
isSystemTraySupported,
|
||||
isMinimizeToAndStartInSystemTraySupported,
|
||||
isInternalUser,
|
||||
lastSyncTime,
|
||||
makeSyncRequest,
|
||||
notificationContent,
|
||||
@@ -373,6 +379,7 @@ export function Preferences({
|
||||
localeOverride,
|
||||
themeSetting,
|
||||
universalExpireTimer = DurationInSeconds.ZERO,
|
||||
validateBackup,
|
||||
whoCanFindMe,
|
||||
whoCanSeeMe,
|
||||
zoomFactor,
|
||||
@@ -410,6 +417,9 @@ export function Preferences({
|
||||
if (page === Page.Backups && !shouldShowBackupsPage) {
|
||||
setPage(Page.General);
|
||||
}
|
||||
if (page === Page.Internal && !isInternalUser) {
|
||||
setPage(Page.General);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (page === Page.Backups) {
|
||||
@@ -1728,6 +1738,10 @@ export function Preferences({
|
||||
locale={resolvedLocale}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Internal) {
|
||||
settings = (
|
||||
<PreferencesInternal i18n={i18n} validateBackup={validateBackup} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1829,6 +1843,19 @@ export function Preferences({
|
||||
{i18n('icu:Preferences__button--backups')}
|
||||
</button>
|
||||
) : null}
|
||||
{isInternalUser ? (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--internal': true,
|
||||
'Preferences__button--selected': page === Page.Internal,
|
||||
})}
|
||||
onClick={() => setPage(Page.Internal)}
|
||||
>
|
||||
{i18n('icu:Preferences__button--internal')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="Preferences__settings-pane" ref={settingsPaneRef}>
|
||||
{settings}
|
||||
|
||||
104
ts/components/PreferencesInternal.tsx
Normal file
104
ts/components/PreferencesInternal.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { toLogFormat } from '../types/errors';
|
||||
import { formatFileSize } from '../util/formatFileSize';
|
||||
import type { ExportResultType as BackupExportResultType } from '../services/backups';
|
||||
import { SettingsRow, SettingsControl } from './PreferencesUtil';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
export function PreferencesInternal({
|
||||
i18n,
|
||||
validateBackup: doValidateBackup,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
validateBackup: () => Promise<BackupExportResultType>;
|
||||
}): JSX.Element {
|
||||
const [isValidationPending, setIsValidationPending] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState<
|
||||
| {
|
||||
result: BackupExportResultType;
|
||||
}
|
||||
| {
|
||||
error: Error;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
|
||||
const validateBackup = useCallback(async () => {
|
||||
setIsValidationPending(true);
|
||||
setValidationResult(undefined);
|
||||
try {
|
||||
setValidationResult({ result: await doValidateBackup() });
|
||||
} catch (error) {
|
||||
setValidationResult({ error });
|
||||
} finally {
|
||||
setIsValidationPending(false);
|
||||
}
|
||||
}, [doValidateBackup]);
|
||||
|
||||
let validationElem: JSX.Element | undefined;
|
||||
if (validationResult != null) {
|
||||
if ('result' in validationResult) {
|
||||
const {
|
||||
result: { totalBytes, stats },
|
||||
} = validationResult;
|
||||
|
||||
validationElem = (
|
||||
<div className="Preferences--internal--validate-backup--result">
|
||||
<p>File size: {formatFileSize(totalBytes)}</p>
|
||||
<pre>
|
||||
<code>{JSON.stringify(stats, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const { error } = validationResult;
|
||||
|
||||
validationElem = (
|
||||
<div className="Preferences--internal--validate-backup--error">
|
||||
<pre>
|
||||
<code>{toLogFormat(error)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="Preferences__title Preferences__title--internal">
|
||||
<div className="Preferences__title--header">
|
||||
{i18n('icu:Preferences__button--internal')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsRow
|
||||
className="Preferences--internal--backups"
|
||||
title={i18n('icu:Preferences__button--backups')}
|
||||
>
|
||||
<SettingsControl
|
||||
left={i18n('icu:Preferences__internal__validate-backup--description')}
|
||||
right={
|
||||
<Button
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={validateBackup}
|
||||
disabled={isValidationPending}
|
||||
>
|
||||
{isValidationPending ? (
|
||||
<Spinner size="22px" svgSize="small" />
|
||||
) : (
|
||||
i18n('icu:Preferences__internal__validate-backup')
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{validationElem}
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -64,9 +64,11 @@ export class SettingsChannel extends EventEmitter {
|
||||
this.#installCallback('deleteAllMyStories');
|
||||
this.#installCallback('getAvailableIODevices');
|
||||
this.#installCallback('isPrimary');
|
||||
this.#installCallback('isInternalUser');
|
||||
this.#installCallback('syncRequest');
|
||||
this.#installCallback('setEmojiSkinToneDefault');
|
||||
this.#installCallback('getEmojiSkinToneDefault');
|
||||
this.#installCallback('validateBackup');
|
||||
|
||||
// Backups
|
||||
this.#installSetting('backupFeatureEnabled', { setter: false });
|
||||
|
||||
@@ -204,6 +204,18 @@ type NonBubbleResultType = Readonly<
|
||||
}
|
||||
>;
|
||||
|
||||
export type StatsType = {
|
||||
adHocCalls: number;
|
||||
callLinks: number;
|
||||
conversations: number;
|
||||
chats: number;
|
||||
distributionLists: number;
|
||||
messages: number;
|
||||
skippedMessages: number;
|
||||
stickerPacks: number;
|
||||
fixedDirectMessages: number;
|
||||
};
|
||||
|
||||
export class BackupExportStream extends Readable {
|
||||
// Shared between all methods for consistency.
|
||||
#now = Date.now();
|
||||
@@ -213,7 +225,7 @@ export class BackupExportStream extends Readable {
|
||||
readonly #serviceIdToRecipientId = new Map<string, number>();
|
||||
readonly #e164ToRecipientId = new Map<string, number>();
|
||||
readonly #roomIdToRecipientId = new Map<string, number>();
|
||||
readonly #stats = {
|
||||
readonly #stats: StatsType = {
|
||||
adHocCalls: 0,
|
||||
callLinks: 0,
|
||||
conversations: 0,
|
||||
@@ -270,6 +282,10 @@ export class BackupExportStream extends Readable {
|
||||
);
|
||||
}
|
||||
|
||||
public getStats(): Readonly<StatsType> {
|
||||
return this.#stats;
|
||||
}
|
||||
|
||||
async #unsafeRun(backupLevel: BackupLevel): Promise<void> {
|
||||
this.#ourConversation =
|
||||
window.ConversationController.getOurConversationOrThrow().attributes;
|
||||
|
||||
@@ -47,7 +47,7 @@ import { constantTimeEqual } from '../../Crypto';
|
||||
import { measureSize } from '../../AttachmentCrypto';
|
||||
import { isTestOrMockEnvironment } from '../../environment';
|
||||
import { runStorageServiceSyncJob } from '../storage';
|
||||
import { BackupExportStream } from './export';
|
||||
import { BackupExportStream, type StatsType } from './export';
|
||||
import { BackupImportStream } from './import';
|
||||
import { getKeyMaterial } from './crypto';
|
||||
import { BackupCredentials } from './credentials';
|
||||
@@ -61,6 +61,8 @@ import {
|
||||
BackupProcessingError,
|
||||
RelinkRequestedError,
|
||||
} from './errors';
|
||||
import { FileStream } from './util/FileStream';
|
||||
import { MemoryStream } from './util/MemoryStream';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import { isAdhoc, isNightly } from '../../util/version';
|
||||
import { getMessageQueueTime } from '../../util/getMessageQueueTime';
|
||||
@@ -96,6 +98,11 @@ export type ImportOptionsType = Readonly<{
|
||||
onProgress?: (currentBytes: number, totalBytes: number) => void;
|
||||
}>;
|
||||
|
||||
export type ExportResultType = Readonly<{
|
||||
totalBytes: number;
|
||||
stats: Readonly<StatsType>;
|
||||
}>;
|
||||
|
||||
export class BackupsService {
|
||||
#isStarted = false;
|
||||
#isRunning: 'import' | 'export' | false = false;
|
||||
@@ -289,14 +296,17 @@ export class BackupsService {
|
||||
public async exportBackupData(
|
||||
backupLevel: BackupLevel = BackupLevel.Free,
|
||||
backupType = BackupType.Ciphertext
|
||||
): Promise<Uint8Array> {
|
||||
): Promise<{ data: Uint8Array } & ExportResultType> {
|
||||
const sink = new PassThrough();
|
||||
|
||||
const chunks = new Array<Uint8Array>();
|
||||
sink.on('data', chunk => chunks.push(chunk));
|
||||
await this.#exportBackup(sink, backupLevel, backupType);
|
||||
const result = await this.#exportBackup(sink, backupLevel, backupType);
|
||||
|
||||
return Bytes.concatenate(chunks);
|
||||
return {
|
||||
...result,
|
||||
data: Bytes.concatenate(chunks),
|
||||
};
|
||||
}
|
||||
|
||||
// Test harness
|
||||
@@ -305,22 +315,38 @@ export class BackupsService {
|
||||
backupLevel: BackupLevel = BackupLevel.Free,
|
||||
backupType = BackupType.Ciphertext
|
||||
): Promise<number> {
|
||||
const size = await this.#exportBackup(
|
||||
const { totalBytes } = await this.#exportBackup(
|
||||
createWriteStream(path),
|
||||
backupLevel,
|
||||
backupType
|
||||
);
|
||||
|
||||
if (backupType === BackupType.Ciphertext) {
|
||||
await validateBackup(path, size);
|
||||
await validateBackup(() => new FileStream(path), totalBytes);
|
||||
}
|
||||
|
||||
return size;
|
||||
return totalBytes;
|
||||
}
|
||||
|
||||
// Test harness
|
||||
public async validate(
|
||||
backupLevel: BackupLevel = BackupLevel.Free,
|
||||
backupType = BackupType.Ciphertext
|
||||
): Promise<ExportResultType> {
|
||||
const { data, ...result } = await this.exportBackupData(
|
||||
backupLevel,
|
||||
backupType
|
||||
);
|
||||
const buffer = Buffer.from(data);
|
||||
|
||||
await validateBackup(() => new MemoryStream(buffer), buffer.byteLength);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Test harness
|
||||
public async exportWithDialog(): Promise<void> {
|
||||
const data = await this.exportBackupData();
|
||||
const { data } = await this.exportBackupData();
|
||||
|
||||
const { saveAttachmentToDisk } = window.Signal.Migrations;
|
||||
|
||||
@@ -712,7 +738,7 @@ export class BackupsService {
|
||||
sink: Writable,
|
||||
backupLevel: BackupLevel = BackupLevel.Free,
|
||||
backupType = BackupType.Ciphertext
|
||||
): Promise<number> {
|
||||
): Promise<ExportResultType> {
|
||||
strictAssert(!this.#isRunning, 'BackupService is already running');
|
||||
|
||||
log.info('exportBackup: starting...');
|
||||
@@ -766,7 +792,7 @@ export class BackupsService {
|
||||
throw missingCaseError(backupType);
|
||||
}
|
||||
|
||||
return totalBytes;
|
||||
return { totalBytes, stats: recordStream.getStats() };
|
||||
} finally {
|
||||
log.info('exportBackup: finished...');
|
||||
this.#isRunning = false;
|
||||
|
||||
23
ts/services/backups/util/MemoryStream.ts
Normal file
23
ts/services/backups/util/MemoryStream.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { type Buffer } from 'node:buffer';
|
||||
import { InputStream } from '@signalapp/libsignal-client/dist/io';
|
||||
|
||||
export class MemoryStream extends InputStream {
|
||||
#offset = 0;
|
||||
|
||||
constructor(private readonly buffer: Buffer) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override async read(amount: number): Promise<Buffer> {
|
||||
const result = this.buffer.subarray(this.#offset, this.#offset + amount);
|
||||
this.#offset += amount;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async skip(amount: number): Promise<void> {
|
||||
this.#offset += amount;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as libsignal from '@signalapp/libsignal-client/dist/MessageBackup';
|
||||
import type { InputStream } from '@signalapp/libsignal-client/dist/io';
|
||||
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { toAciObject } from '../../util/ServiceId';
|
||||
import { isTestOrMockEnvironment } from '../../environment';
|
||||
import { FileStream } from './util/FileStream';
|
||||
|
||||
export async function validateBackup(
|
||||
filePath: string,
|
||||
inputFactory: () => InputStream,
|
||||
fileSize: number
|
||||
): Promise<void> {
|
||||
const accountEntropy = window.storage.get('accountEntropyPool');
|
||||
@@ -24,7 +24,7 @@ export async function validateBackup(
|
||||
const outcome = await libsignal.validate(
|
||||
backupKey,
|
||||
libsignal.Purpose.RemoteBackup,
|
||||
() => new FileStream(filePath),
|
||||
inputFactory,
|
||||
BigInt(fileSize)
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { readFile } from 'node:fs/promises';
|
||||
import { basename, join } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { BackupLevel } from '@signalapp/libsignal-client/zkgroup';
|
||||
import { InputStream } from '@signalapp/libsignal-client/dist/io';
|
||||
import {
|
||||
ComparableBackup,
|
||||
Purpose,
|
||||
@@ -16,29 +15,12 @@ import { assert } from 'chai';
|
||||
import { clearData } from './helpers';
|
||||
import { loadAllAndReinitializeRedux } from '../../services/allLoaders';
|
||||
import { backupsService, BackupType } from '../../services/backups';
|
||||
import { MemoryStream } from '../../services/backups/util/MemoryStream';
|
||||
import { initialize as initializeExpiringMessageService } from '../../services/expiringMessagesDeletion';
|
||||
import { DataWriter } from '../../sql/Client';
|
||||
|
||||
const { BACKUP_INTEGRATION_DIR } = process.env;
|
||||
|
||||
class MemoryStream extends InputStream {
|
||||
#offset = 0;
|
||||
|
||||
constructor(private readonly buffer: Buffer) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override async read(amount: number): Promise<Buffer> {
|
||||
const result = this.buffer.slice(this.#offset, this.#offset + amount);
|
||||
this.#offset += amount;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async skip(amount: number): Promise<void> {
|
||||
this.#offset += amount;
|
||||
}
|
||||
}
|
||||
|
||||
describe('backup/integration', () => {
|
||||
before(async () => {
|
||||
await initializeExpiringMessageService();
|
||||
@@ -75,7 +57,7 @@ describe('backup/integration', () => {
|
||||
backupType: BackupType.TestOnlyPlaintext,
|
||||
});
|
||||
|
||||
const exported = await backupsService.exportBackupData(
|
||||
const { data: exported } = await backupsService.exportBackupData(
|
||||
BackupLevel.Paid,
|
||||
BackupType.TestOnlyPlaintext
|
||||
);
|
||||
|
||||
@@ -23,6 +23,10 @@ import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { calling } from '../services/calling';
|
||||
import { resolveUsernameByLinkBase64 } from '../services/username';
|
||||
import { writeProfile } from '../services/writeProfile';
|
||||
import {
|
||||
backupsService,
|
||||
type ExportResultType as BackupExportResultType,
|
||||
} from '../services/backups';
|
||||
import { isInCall } from '../state/selectors/calling';
|
||||
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
|
||||
import { getCustomColors } from '../state/selectors/items';
|
||||
@@ -65,6 +69,7 @@ import type {
|
||||
BackupStatusType,
|
||||
} from '../types/backups';
|
||||
import { isBackupFeatureEnabled } from './isBackupEnabled';
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
|
||||
type SentMediaQualityType = 'standard' | 'high';
|
||||
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||
@@ -135,6 +140,7 @@ export type IPCEventsCallbacksType = {
|
||||
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
|
||||
installStickerPack: (packId: string, key: string) => Promise<void>;
|
||||
isPrimary: () => boolean;
|
||||
isInternalUser: () => boolean;
|
||||
removeCustomColor: (x: string) => void;
|
||||
removeCustomColorOnConversations: (x: string) => void;
|
||||
removeDarkOverlay: () => void;
|
||||
@@ -158,6 +164,7 @@ export type IPCEventsCallbacksType = {
|
||||
unknownSignalLink: () => void;
|
||||
getCustomColors: () => Record<string, CustomColorType>;
|
||||
syncRequest: () => Promise<void>;
|
||||
validateBackup: () => Promise<BackupExportResultType>;
|
||||
setGlobalDefaultConversationColor: (
|
||||
color: ConversationColorType,
|
||||
customColor?: { id: string; value: CustomColorType }
|
||||
@@ -535,6 +542,7 @@ export function createIPCEvents(
|
||||
},
|
||||
|
||||
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
|
||||
isInternalUser: () => RemoteConfig.isEnabled('desktop.internalUser'),
|
||||
syncRequest: async () => {
|
||||
const contactSyncComplete = waitForEvent(
|
||||
'contactSync:complete',
|
||||
@@ -543,6 +551,7 @@ export function createIPCEvents(
|
||||
await sendSyncRequests();
|
||||
return contactSyncComplete;
|
||||
},
|
||||
validateBackup: () => backupsService.validate(),
|
||||
getLastSyncTime: () => window.storage.get('synced_at'),
|
||||
setLastSyncTime: value => window.storage.put('synced_at', value),
|
||||
getUniversalExpireTimer: () => universalExpireTimer.get(),
|
||||
|
||||
@@ -47,9 +47,11 @@ installCallback('refreshCloudBackupStatus');
|
||||
installCallback('refreshBackupSubscriptionStatus');
|
||||
installCallback('deleteAllMyStories');
|
||||
installCallback('isPrimary');
|
||||
installCallback('isInternalUser');
|
||||
installCallback('syncRequest');
|
||||
installCallback('getEmojiSkinToneDefault');
|
||||
installCallback('setEmojiSkinToneDefault');
|
||||
installCallback('validateBackup');
|
||||
|
||||
installSetting('alwaysRelayCalls');
|
||||
installSetting('audioMessage');
|
||||
|
||||
@@ -75,6 +75,7 @@ SettingsWindowProps.onRender(
|
||||
isNotificationAttentionSupported,
|
||||
isSyncSupported,
|
||||
isSystemTraySupported,
|
||||
isInternalUser,
|
||||
lastSyncTime,
|
||||
makeSyncRequest,
|
||||
notificationContent,
|
||||
@@ -128,6 +129,7 @@ SettingsWindowProps.onRender(
|
||||
localeOverride,
|
||||
themeSetting,
|
||||
universalExpireTimer,
|
||||
validateBackup,
|
||||
whoCanFindMe,
|
||||
whoCanSeeMe,
|
||||
zoomFactor,
|
||||
@@ -188,6 +190,7 @@ SettingsWindowProps.onRender(
|
||||
isNotificationAttentionSupported={isNotificationAttentionSupported}
|
||||
isSyncSupported={isSyncSupported}
|
||||
isSystemTraySupported={isSystemTraySupported}
|
||||
isInternalUser={isInternalUser}
|
||||
lastSyncTime={lastSyncTime}
|
||||
localeOverride={localeOverride}
|
||||
makeSyncRequest={makeSyncRequest}
|
||||
@@ -243,6 +246,7 @@ SettingsWindowProps.onRender(
|
||||
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
|
||||
themeSetting={themeSetting}
|
||||
universalExpireTimer={universalExpireTimer}
|
||||
validateBackup={validateBackup}
|
||||
whoCanFindMe={whoCanFindMe}
|
||||
whoCanSeeMe={whoCanSeeMe}
|
||||
zoomFactor={zoomFactor}
|
||||
|
||||
@@ -98,7 +98,9 @@ const ipcGetAvailableIODevices = createCallback('getAvailableIODevices');
|
||||
const ipcGetCustomColors = createCallback('getCustomColors');
|
||||
const ipcGetEmojiSkinToneDefault = createCallback('getEmojiSkinToneDefault');
|
||||
const ipcIsSyncNotSupported = createCallback('isPrimary');
|
||||
const ipcIsInternalUser = createCallback('isInternalUser');
|
||||
const ipcMakeSyncRequest = createCallback('syncRequest');
|
||||
const ipcValidateBackup = createCallback('validateBackup');
|
||||
const ipcDeleteAllMyStories = createCallback('deleteAllMyStories');
|
||||
const ipcRefreshCloudBackupStatus = createCallback('refreshCloudBackupStatus');
|
||||
const ipcRefreshBackupSubscriptionStatus = createCallback(
|
||||
@@ -205,6 +207,7 @@ async function renderPreferences() {
|
||||
customColors,
|
||||
defaultConversationColor,
|
||||
isSyncNotSupported,
|
||||
isInternalUser,
|
||||
} = await awaitObject({
|
||||
autoDownloadAttachment: settingAutoDownloadAttachment.getValue(),
|
||||
backupFeatureEnabled: settingBackupFeatureEnabled.getValue(),
|
||||
@@ -253,6 +256,7 @@ async function renderPreferences() {
|
||||
defaultConversationColor: ipcGetDefaultConversationColor(),
|
||||
emojiSkinToneDefault: ipcGetEmojiSkinToneDefault(),
|
||||
isSyncNotSupported: ipcIsSyncNotSupported(),
|
||||
isInternalUser: ipcIsInternalUser(),
|
||||
});
|
||||
|
||||
const { availableCameras, availableMicrophones, availableSpeakers } =
|
||||
@@ -365,6 +369,7 @@ async function renderPreferences() {
|
||||
resetAllChatColors: ipcResetAllChatColors,
|
||||
resetDefaultChatColor: ipcResetDefaultChatColor,
|
||||
setGlobalDefaultConversationColor: ipcSetGlobalDefaultConversationColor,
|
||||
validateBackup: ipcValidateBackup,
|
||||
// Limited support features
|
||||
isAutoDownloadUpdatesSupported: Settings.isAutoDownloadUpdatesSupported(
|
||||
OS,
|
||||
@@ -374,6 +379,7 @@ async function renderPreferences() {
|
||||
isHideMenuBarSupported: Settings.isHideMenuBarSupported(OS),
|
||||
isNotificationAttentionSupported: Settings.isDrawAttentionSupported(OS),
|
||||
isSyncSupported: !isSyncNotSupported,
|
||||
isInternalUser,
|
||||
isSystemTraySupported: Settings.isSystemTraySupported(OS),
|
||||
isMinimizeToAndStartInSystemTraySupported:
|
||||
Settings.isMinimizeToAndStartInSystemTraySupported(OS),
|
||||
|
||||
Reference in New Issue
Block a user