mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-10 15:18:28 +01:00
RelinkDialog: Show something different when primary device
This commit is contained in:
@@ -3390,6 +3390,12 @@
|
||||
"icu:unlinked": {
|
||||
"messageformat": "Unlinked"
|
||||
},
|
||||
"icu:unregisteredWarning": {
|
||||
"messageformat": "Click to re-register and continue messaging. Signal Desktop is unregistered likely because you registered your phone number with Signal on a different device."
|
||||
},
|
||||
"icu:unregistered": {
|
||||
"messageformat": "No longer registered"
|
||||
},
|
||||
"icu:autoUpdateNewVersionTitle": {
|
||||
"messageformat": "Update Available"
|
||||
},
|
||||
|
||||
@@ -2692,7 +2692,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
this.emit('removeAllData');
|
||||
}
|
||||
|
||||
async removeAllConfiguration(): Promise<void> {
|
||||
async removeAllConfiguration(isPrimary: boolean): Promise<void> {
|
||||
// Conversations. These properties are not present in redux.
|
||||
window.ConversationController.getAll().forEach(conversation => {
|
||||
conversation.set({
|
||||
@@ -2703,7 +2703,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
});
|
||||
});
|
||||
|
||||
await DataWriter.removeAllConfiguration();
|
||||
await DataWriter.removeAllConfiguration(isPrimary);
|
||||
|
||||
await this.hydrateCaches();
|
||||
|
||||
|
||||
@@ -3446,14 +3446,18 @@ async function startApp(): Promise<void> {
|
||||
try {
|
||||
log.info('unlinkAndDisconnect: removing configuration');
|
||||
|
||||
// We use username for integrity check
|
||||
const ourConversation =
|
||||
window.ConversationController.getOurConversation();
|
||||
if (ourConversation) {
|
||||
await ourConversation.updateUsername(undefined, {
|
||||
shouldSave: true,
|
||||
fromStorageService: false,
|
||||
});
|
||||
const weArePrimary = window.ConversationController.areWePrimaryDevice();
|
||||
if (!weArePrimary) {
|
||||
log.info('unlinkAndDisconnect: removing username');
|
||||
// We use username for integrity check
|
||||
const ourConversation =
|
||||
window.ConversationController.getOurConversation();
|
||||
if (ourConversation) {
|
||||
await ourConversation.updateUsername(undefined, {
|
||||
shouldSave: true,
|
||||
fromStorageService: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Then make sure outstanding conversation saves are flushed
|
||||
@@ -3463,7 +3467,7 @@ async function startApp(): Promise<void> {
|
||||
await DataReader.getItemById('manifestVersion');
|
||||
|
||||
// Finally, conversations in the database, and delete all config tables
|
||||
await signalProtocolStore.removeAllConfiguration();
|
||||
await signalProtocolStore.removeAllConfiguration(weArePrimary);
|
||||
|
||||
// Re-hydrate items from memory; removeAllConfiguration above changed database
|
||||
await itemStorage.fetch();
|
||||
|
||||
@@ -15,6 +15,8 @@ const defaultProps = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
i18n,
|
||||
relinkDevice: action('relink-device'),
|
||||
reregister: action('reregister'),
|
||||
weArePrimaryDevice: false,
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
@@ -39,8 +41,9 @@ export default {
|
||||
export function Iterations(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{permutations.map(({ props, title }) => (
|
||||
{permutations.map(({ props, title }, index) => (
|
||||
<>
|
||||
{index > 0 && <br />}
|
||||
<h3>{title}</h3>
|
||||
<FakeLeftPaneContainer
|
||||
containerWidthBreakpoint={props.containerWidthBreakpoint}
|
||||
@@ -52,3 +55,21 @@ export function Iterations(): React.JSX.Element {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function IterationsStandalone(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{permutations.map(({ props, title }, index) => (
|
||||
<>
|
||||
{index > 0 && <br />}
|
||||
<h3>{title}</h3>
|
||||
<FakeLeftPaneContainer
|
||||
containerWidthBreakpoint={props.containerWidthBreakpoint}
|
||||
>
|
||||
<DialogRelink {...defaultProps} {...props} weArePrimaryDevice />
|
||||
</FakeLeftPaneContainer>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,31 @@ export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
relinkDevice: () => void;
|
||||
reregister: () => void;
|
||||
weArePrimaryDevice: boolean;
|
||||
};
|
||||
|
||||
export function DialogRelink({
|
||||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
relinkDevice,
|
||||
reregister,
|
||||
weArePrimaryDevice,
|
||||
}: PropsType): React.JSX.Element | null {
|
||||
if (weArePrimaryDevice) {
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
icon="relink"
|
||||
clickLabel={i18n('icu:unregisteredWarning')}
|
||||
onClick={reregister}
|
||||
title={i18n('icu:unregistered')}
|
||||
hasAction
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
|
||||
@@ -260,6 +260,8 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
<DialogRelink
|
||||
i18n={i18n}
|
||||
relinkDevice={action('relinkDevice')}
|
||||
reregister={action('reregister')}
|
||||
weArePrimaryDevice={false}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1486,7 +1486,7 @@ type WritableInterface = {
|
||||
resetProtectedAttachmentPaths: () => void;
|
||||
|
||||
removeAll: () => void;
|
||||
removeAllConfiguration: () => void;
|
||||
removeAllConfiguration: (isPrimary: boolean) => void;
|
||||
eraseStorageServiceState: () => void;
|
||||
|
||||
insertJob(job: Readonly<StoredJob>): void;
|
||||
|
||||
+12
-3
@@ -30,7 +30,10 @@ import type { ReactionType } from '../types/Reactions.std.ts';
|
||||
import { ReactionReadStatus } from '../types/Reactions.std.ts';
|
||||
import type { AciString, ServiceIdString } from '../types/ServiceId.std.ts';
|
||||
import { isServiceIdString } from '../types/ServiceId.std.ts';
|
||||
import { STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK } from '../types/StorageKeys.std.ts';
|
||||
import {
|
||||
STORAGE_KEYS_TO_PRESERVE_WHEN_PRIMARY,
|
||||
STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK,
|
||||
} from '../types/StorageKeys.std.ts';
|
||||
import type { StoryDistributionIdString } from '../types/StoryDistributionId.std.ts';
|
||||
import * as Errors from '../types/errors.std.ts';
|
||||
import { assertDev, strictAssert } from '../util/assert.std.ts';
|
||||
@@ -8546,7 +8549,7 @@ function removeAll(db: WritableDB): void {
|
||||
}
|
||||
|
||||
// Anything that isn't user-visible data
|
||||
function removeAllConfiguration(db: WritableDB): void {
|
||||
function removeAllConfiguration(db: WritableDB, isPrimary: boolean): void {
|
||||
db.transaction(() => {
|
||||
db.exec(
|
||||
`
|
||||
@@ -8576,7 +8579,13 @@ function removeAllConfiguration(db: WritableDB): void {
|
||||
})
|
||||
.all();
|
||||
|
||||
const allowedSet = new Set<string>(STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK);
|
||||
let allowedSet = new Set<string>(STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK);
|
||||
|
||||
if (isPrimary) {
|
||||
allowedSet = allowedSet.union(
|
||||
new Set<string>(STORAGE_KEYS_TO_PRESERVE_WHEN_PRIMARY)
|
||||
);
|
||||
}
|
||||
for (const id of itemIds) {
|
||||
if (!allowedSet.has(id)) {
|
||||
removeById(db, 'items', id);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { SocketStatus } from '../../types/SocketStatus.std.ts';
|
||||
import { trigger } from '../../shims/events.dom.ts';
|
||||
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation.std.ts';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.ts';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions.std.ts';
|
||||
import { noopAction } from './noop.std.ts';
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.ts';
|
||||
import type { NoopActionType } from './noop.std.ts';
|
||||
|
||||
// State
|
||||
|
||||
@@ -20,7 +23,6 @@ export type NetworkStateType = ReadonlyDeep<{
|
||||
// Actions
|
||||
|
||||
const SET_NETWORK_STATUS = 'network/SET_NETWORK_STATUS';
|
||||
const RELINK_DEVICE = 'network/RELINK_DEVICE';
|
||||
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
|
||||
const SET_OUTAGE = 'network/SET_OUTAGE';
|
||||
|
||||
@@ -70,12 +72,16 @@ function setNetworkStatus(
|
||||
};
|
||||
}
|
||||
|
||||
function relinkDevice(): RelinkDeviceActionType {
|
||||
function relinkDevice(): NoopActionType {
|
||||
trigger('setupAsNewDevice');
|
||||
|
||||
return {
|
||||
type: RELINK_DEVICE,
|
||||
};
|
||||
return noopAction('relinkDevice');
|
||||
}
|
||||
|
||||
function reregister(): NoopActionType {
|
||||
trigger('setupAsStandalone');
|
||||
|
||||
return noopAction('reregister');
|
||||
}
|
||||
|
||||
function setChallengeStatus(
|
||||
@@ -100,6 +106,7 @@ function setOutage(isOutage: boolean): SetOutageActionType {
|
||||
export const actions = {
|
||||
setNetworkStatus,
|
||||
relinkDevice,
|
||||
reregister,
|
||||
setChallengeStatus,
|
||||
setOutage,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { DialogRelink } from '../../components/DialogRelink.dom.tsx';
|
||||
import { getIntl } from '../selectors/user.std.ts';
|
||||
import type { WidthBreakpoint } from '../../components/_util.std.ts';
|
||||
import { areWePrimaryDevice, getIntl } from '../selectors/user.std.ts';
|
||||
import { useNetworkActions } from '../ducks/network.dom.ts';
|
||||
|
||||
import type { WidthBreakpoint } from '../../components/_util.std.ts';
|
||||
|
||||
type SmartRelinkDialogProps = Readonly<{
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}>;
|
||||
@@ -15,12 +17,16 @@ export const SmartRelinkDialog = memo(function SmartRelinkDialog({
|
||||
containerWidthBreakpoint,
|
||||
}: SmartRelinkDialogProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { relinkDevice } = useNetworkActions();
|
||||
const { relinkDevice, reregister } = useNetworkActions();
|
||||
const weArePrimaryDevice = useSelector(areWePrimaryDevice);
|
||||
|
||||
return (
|
||||
<DialogRelink
|
||||
i18n={i18n}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
relinkDevice={relinkDevice}
|
||||
reregister={reregister}
|
||||
weArePrimaryDevice={weArePrimaryDevice}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,8 @@ describe('Remove all configuration test', () => {
|
||||
}
|
||||
);
|
||||
|
||||
await DataWriter.removeAllConfiguration();
|
||||
const isPrimary = false;
|
||||
await DataWriter.removeAllConfiguration(isPrimary);
|
||||
|
||||
const convoAfter = await DataReader.getConversationById(attributes.id);
|
||||
assert.strictEqual(convoAfter?.expireTimerVersion, 1);
|
||||
@@ -59,6 +60,10 @@ describe('Remove all configuration test', () => {
|
||||
});
|
||||
|
||||
/** Should be deleted */
|
||||
await DataWriter.createOrUpdateItem({
|
||||
id: 'blocked',
|
||||
value: '["a", "b", "c"]',
|
||||
});
|
||||
await DataWriter.createOrUpdateItem({
|
||||
id: 'storageFetchComplete',
|
||||
value: true,
|
||||
@@ -69,7 +74,8 @@ describe('Remove all configuration test', () => {
|
||||
value: 1.5,
|
||||
});
|
||||
|
||||
await DataWriter.removeAllConfiguration();
|
||||
const isPrimary = false;
|
||||
await DataWriter.removeAllConfiguration(isPrimary);
|
||||
|
||||
const allItems = await DataReader.getAllItems();
|
||||
assert.deepStrictEqual(allItems, {
|
||||
@@ -78,4 +84,36 @@ describe('Remove all configuration test', () => {
|
||||
zoomFactor: 1.5,
|
||||
});
|
||||
});
|
||||
|
||||
it('When isPrimary=true, preserves additional configuration', async () => {
|
||||
/** Should be preserved */
|
||||
await DataWriter.createOrUpdateItem({
|
||||
id: 'zoomFactor',
|
||||
value: 1.5,
|
||||
});
|
||||
await DataWriter.createOrUpdateItem({
|
||||
id: 'read-receipt-setting',
|
||||
value: true,
|
||||
});
|
||||
|
||||
/** Should be deleted */
|
||||
await DataWriter.createOrUpdateItem({
|
||||
id: 'storageFetchComplete',
|
||||
value: true,
|
||||
});
|
||||
await DataWriter.createOrUpdateItem({
|
||||
// @ts-expect-error incorrect key
|
||||
id: 'unknown-key',
|
||||
value: 1.5,
|
||||
});
|
||||
|
||||
const isPrimary = true;
|
||||
await DataWriter.removeAllConfiguration(isPrimary);
|
||||
|
||||
const allItems = await DataReader.getAllItems();
|
||||
assert.deepStrictEqual(allItems, {
|
||||
'read-receipt-setting': true,
|
||||
zoomFactor: 1.5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1015,6 +1015,7 @@ export default class AccountManager extends EventTarget {
|
||||
const previousNumber = itemStorage.user.getNumber();
|
||||
const previousACI = itemStorage.user.getAci();
|
||||
const previousPNI = itemStorage.user.getPni();
|
||||
const previousDeviceId = itemStorage.user.getDeviceId();
|
||||
|
||||
log.info(
|
||||
`createAccount: Number is ${number}, password has length: ${
|
||||
@@ -1077,8 +1078,11 @@ export default class AccountManager extends EventTarget {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.info('createAccount: Erasing configuration');
|
||||
await signalProtocolStore.removeAllConfiguration();
|
||||
const weArePrimary = isNumber(previousDeviceId) && previousDeviceId === 1;
|
||||
log.info(
|
||||
`createAccount: Erasing configuration (isPrimary=${weArePrimary})`
|
||||
);
|
||||
await signalProtocolStore.removeAllConfiguration(weArePrimary);
|
||||
}
|
||||
|
||||
await senderCertificateService.clear();
|
||||
|
||||
+36
-17
@@ -411,16 +411,31 @@ export const STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK = [
|
||||
'restoredBackupFirstAppVersion',
|
||||
] as const satisfies ReadonlyArray<keyof StorageAccessType>;
|
||||
|
||||
const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [
|
||||
export const STORAGE_KEYS_TO_PRESERVE_WHEN_PRIMARY = [
|
||||
'auto-download-attachment',
|
||||
'blocked-groups',
|
||||
'blocked-uuids',
|
||||
'lastCallQualitySurveyTime',
|
||||
'lastCallQualityFailureSurveyTime',
|
||||
'cqsTestMode',
|
||||
'read-receipt-setting',
|
||||
'blocked',
|
||||
'device_name',
|
||||
'seenPinMessageDisappearingMessagesWarningCount',
|
||||
'usernameLastIntegrityCheck',
|
||||
'usernameCorrupted',
|
||||
'usernameLinkCorrupted',
|
||||
'usernameLink',
|
||||
'needProfileMovedModal',
|
||||
'notificationProfileOverride',
|
||||
'notificationProfileOverrideFromPrimary',
|
||||
'notificationProfileSyncDisabled',
|
||||
'sendEditWarningShown',
|
||||
'formattingWarningShown',
|
||||
'localDeleteWarningShown',
|
||||
] as const satisfies ReadonlyArray<keyof StorageAccessType>;
|
||||
|
||||
const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [
|
||||
'lastCallQualitySurveyTime',
|
||||
'lastCallQualityFailureSurveyTime',
|
||||
'cqsTestMode',
|
||||
'deviceCreatedAt',
|
||||
'hasSeenNotificationProfileOnboarding',
|
||||
'hasSeenKeyTransparencyOnboarding',
|
||||
@@ -439,7 +454,6 @@ const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [
|
||||
'registrationIdMap',
|
||||
'remoteBuildExpiration',
|
||||
'sessionResets',
|
||||
'seenPinMessageDisappearingMessagesWarningCount',
|
||||
'signedKeyId',
|
||||
'signedKeyIdPNI',
|
||||
'signedKeyUpdateTime',
|
||||
@@ -490,16 +504,8 @@ const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [
|
||||
'backupsSubscriberId',
|
||||
'backupsSubscriberPurchaseToken',
|
||||
'backupsSubscriberOriginalTransactionId',
|
||||
'usernameLastIntegrityCheck',
|
||||
'usernameCorrupted',
|
||||
'usernameLinkCorrupted',
|
||||
'usernameLink',
|
||||
'serverAlerts',
|
||||
'needOrphanedAttachmentCheck',
|
||||
'needProfileMovedModal',
|
||||
'notificationProfileOverride',
|
||||
'notificationProfileOverrideFromPrimary',
|
||||
'notificationProfileSyncDisabled',
|
||||
'observedCapabilities',
|
||||
'releaseNotesNextFetchTime',
|
||||
'releaseNotesVersionWatermark',
|
||||
@@ -524,15 +530,12 @@ const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [
|
||||
'signedKeyRotationRejected',
|
||||
'lastHeartbeat',
|
||||
'lastStartup',
|
||||
'sendEditWarningShown',
|
||||
'formattingWarningShown',
|
||||
'hasRegisterSupportForUnauthenticatedDelivery',
|
||||
'masterKeyLastRequestTime',
|
||||
'versionedExpirationTimer',
|
||||
'primarySendsSms',
|
||||
'backupMediaDownloadIdle',
|
||||
'callQualitySurveyCooldownDisabled',
|
||||
'localDeleteWarningShown',
|
||||
'dredDuration',
|
||||
'directMaxBitrate',
|
||||
'isDirectVp9Enabled',
|
||||
@@ -549,6 +552,8 @@ type AssertTrue<T extends true> = T;
|
||||
|
||||
type StorageKeysToPreserveAfterUnlink =
|
||||
(typeof STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK)[number];
|
||||
type StorageKeysToKeepWhenPrimary =
|
||||
(typeof STORAGE_KEYS_TO_PRESERVE_WHEN_PRIMARY)[number];
|
||||
type StorageKeysToRemoveAfterUnlink =
|
||||
(typeof STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK)[number];
|
||||
|
||||
@@ -558,10 +563,24 @@ export type AssertStorageUnlinkKeysDoNotOverlap = AssertTrue<
|
||||
never
|
||||
>
|
||||
>;
|
||||
export type AssertStoragePrimaryKeysDoNotOverlap = AssertTrue<
|
||||
AssertSameMembers<
|
||||
Extract<StorageKeysToPreserveAfterUnlink, StorageKeysToKeepWhenPrimary>,
|
||||
never
|
||||
>
|
||||
>;
|
||||
export type AssertPrimaryAndUnlinkDoNotOverlap = AssertTrue<
|
||||
AssertSameMembers<
|
||||
Extract<StorageKeysToKeepWhenPrimary, StorageKeysToRemoveAfterUnlink>,
|
||||
never
|
||||
>
|
||||
>;
|
||||
|
||||
export type AssertStorageUnlinkKeysAreExhaustive = AssertTrue<
|
||||
AssertSameMembers<
|
||||
StorageKeysToPreserveAfterUnlink | StorageKeysToRemoveAfterUnlink,
|
||||
| StorageKeysToPreserveAfterUnlink
|
||||
| StorageKeysToKeepWhenPrimary
|
||||
| StorageKeysToRemoveAfterUnlink,
|
||||
keyof StorageAccessType
|
||||
>
|
||||
>;
|
||||
|
||||
Vendored
+2
-1
@@ -9,6 +9,7 @@ import type { SystemPreferences } from 'electron';
|
||||
import type { assert } from 'chai';
|
||||
import type { MochaOptions } from 'mocha';
|
||||
|
||||
import type { WhisperEventMap } from './shims/events.dom.ts';
|
||||
import type { IPCRequest as IPCChallengeRequest } from './challenge.dom.ts';
|
||||
import type { OSType } from './util/os/shared.std.ts';
|
||||
import type { SystemThemeType, ThemeType } from './types/Util.std.ts';
|
||||
@@ -282,5 +283,5 @@ declare global {
|
||||
}
|
||||
|
||||
export type WhisperType = {
|
||||
events: EventEmitter;
|
||||
events: EventEmitter<WhisperEventMap>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user