Skip to content

Commit 3dbab74

Browse files
Implement backup support for chat folders
1 parent d328b45 commit 3dbab74

File tree

6 files changed

+142
-32
lines changed

6 files changed

+142
-32
lines changed

ts/components/Preferences.dom.stories.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ import type { SmartPreferencesEditChatFolderPageProps } from '../state/smart/Pre
5858
import { CurrentChatFolders } from '../types/CurrentChatFolders.std.js';
5959
import type { ExternalProps as SmartNotificationProfilesProps } from '../state/smart/PreferencesNotificationProfiles.preload.js';
6060
import type { NotificationProfileIdString } from '../types/NotificationProfile.std.js';
61+
import type {
62+
ExportResultType,
63+
LocalBackupExportResultType,
64+
} from '../services/backups/types.std.js';
6165
import { BackupLevel } from '../services/backups/types.std.js';
6266

6367
const { shuffle } = lodash;
@@ -112,13 +116,14 @@ const availableSpeakers = [
112116
},
113117
];
114118

115-
const validateBackupResult = {
119+
const validateBackupResult: ExportResultType = {
116120
totalBytes: 100,
117121
duration: 10000,
118122
stats: {
119123
adHocCalls: 1,
120124
callLinks: 2,
121125
conversations: 3,
126+
chatFolders: 3,
122127
chats: 4,
123128
distributionLists: 5,
124129
messages: 6,
@@ -129,7 +134,7 @@ const validateBackupResult = {
129134
},
130135
};
131136

132-
const exportLocalBackupResult = {
137+
const exportLocalBackupResult: LocalBackupExportResultType = {
133138
...validateBackupResult,
134139
snapshotDir: '/home/signaluser/SignalBackups/signal-backup-1745618069169',
135140
};

ts/services/backups/export.preload.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ import type {
120120
AboutMe,
121121
BackupExportOptions,
122122
LocalChatStyle,
123+
StatsType,
123124
} from './types.std.js';
124125
import { messageHasPaymentEvent } from '../../messages/payments.std.js';
125126
import {
@@ -177,6 +178,7 @@ import {
177178
} from '../../util/Settings.preload.js';
178179
import { KIBIBYTE } from '../../types/AttachmentSize.std.js';
179180
import { itemStorage } from '../../textsecure/Storage.preload.js';
181+
import { ChatFolderType } from '../../types/ChatFolder.std.js';
180182

181183
const { isNumber } = lodash;
182184

@@ -242,19 +244,6 @@ type NonBubbleResultType = Readonly<
242244
}
243245
>;
244246

245-
export type StatsType = {
246-
adHocCalls: number;
247-
callLinks: number;
248-
conversations: number;
249-
chats: number;
250-
distributionLists: number;
251-
messages: number;
252-
notificationProfiles: number;
253-
skippedMessages: number;
254-
stickerPacks: number;
255-
fixedDirectMessages: number;
256-
};
257-
258247
export class BackupExportStream extends Readable {
259248
// Shared between all methods for consistency.
260249
#now = Date.now();
@@ -269,6 +258,7 @@ export class BackupExportStream extends Readable {
269258
adHocCalls: 0,
270259
callLinks: 0,
271260
conversations: 0,
261+
chatFolders: 0,
272262
chats: 0,
273263
distributionLists: 0,
274264
messages: 0,
@@ -728,6 +718,42 @@ export class BackupExportStream extends Readable {
728718
this.#stats.notificationProfiles += 1;
729719
}
730720

721+
const currentChatFolders = await DataReader.getCurrentChatFolders();
722+
723+
for (const chatFolder of currentChatFolders) {
724+
let folderType: Backups.ChatFolder.FolderType;
725+
if (chatFolder.folderType === ChatFolderType.ALL) {
726+
folderType = Backups.ChatFolder.FolderType.ALL;
727+
} else if (chatFolder.folderType === ChatFolderType.CUSTOM) {
728+
folderType = Backups.ChatFolder.FolderType.CUSTOM;
729+
} else {
730+
log.warn('backups: Dropping chat folder; unknown folder type');
731+
continue;
732+
}
733+
734+
this.#pushFrame({
735+
chatFolder: {
736+
id: uuidToBytes(chatFolder.id),
737+
name: chatFolder.name,
738+
folderType,
739+
showOnlyUnread: chatFolder.showOnlyUnread,
740+
showMutedChats: chatFolder.showMutedChats,
741+
includeAllIndividualChats: chatFolder.includeAllIndividualChats,
742+
includeAllGroupChats: chatFolder.includeAllGroupChats,
743+
includedRecipientIds: chatFolder.includedConversationIds.map(id => {
744+
return this.#getOrPushPrivateRecipient({ id });
745+
}),
746+
excludedRecipientIds: chatFolder.excludedConversationIds.map(id => {
747+
return this.#getOrPushPrivateRecipient({ id });
748+
}),
749+
},
750+
});
751+
752+
// eslint-disable-next-line no-await-in-loop
753+
await this.#flush();
754+
this.#stats.chatFolders += 1;
755+
}
756+
731757
let cursor: PageMessagesCursorType | undefined;
732758

733759
const callHistory = await DataReader.getAllCallHistory();

ts/services/backups/import.preload.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ import {
164164
import { normalizeNotificationProfileId } from '../../types/NotificationProfile-node.node.js';
165165
import { updateBackupMediaDownloadProgress } from '../../util/updateBackupMediaDownloadProgress.preload.js';
166166
import { itemStorage } from '../../textsecure/Storage.preload.js';
167+
import { ChatFolderType } from '../../types/ChatFolder.std.js';
168+
import type { ChatFolderId, ChatFolder } from '../../types/ChatFolder.std.js';
167169

168170
const { isNumber } = lodash;
169171

@@ -543,9 +545,7 @@ export class BackupImportStream extends Writable {
543545
} else if (frame.notificationProfile) {
544546
await this.#fromNotificationProfile(frame.notificationProfile);
545547
} else if (frame.chatFolder) {
546-
log.warn(
547-
`${this.#logId}: Received currently unsupported feature: chat folder. Dropping.`
548-
);
548+
await this.#fromChatFolder(frame.chatFolder);
549549
} else {
550550
log.warn(
551551
`${this.#logId}: unknown unsupported frame item ${frame.item}`
@@ -3634,6 +3634,68 @@ export class BackupImportStream extends Writable {
36343634
await DataWriter.createNotificationProfile(profile);
36353635
}
36363636

3637+
#chatFolderPositionCursor = 0;
3638+
3639+
async #fromChatFolder(proto: Backups.IChatFolder): Promise<void> {
3640+
if (proto.id == null || proto.id.length === 0) {
3641+
log.warn('Dropping chat folder; it was missing an id');
3642+
return;
3643+
}
3644+
const id = bytesToUuid(proto.id) as ChatFolderId | undefined;
3645+
if (id == null) {
3646+
log.warn('Dropping chat folder; invalid uuid bytes');
3647+
return;
3648+
}
3649+
3650+
let folderType: ChatFolderType;
3651+
if (proto.folderType === Backups.ChatFolder.FolderType.ALL) {
3652+
folderType = ChatFolderType.ALL;
3653+
} else if (proto.folderType === Backups.ChatFolder.FolderType.CUSTOM) {
3654+
folderType = ChatFolderType.CUSTOM;
3655+
} else {
3656+
log.warn('Dropping chat folder; unknown folder type');
3657+
return;
3658+
}
3659+
3660+
const position = this.#chatFolderPositionCursor;
3661+
this.#chatFolderPositionCursor += 1;
3662+
3663+
const includedRecipientIds = proto.includedRecipientIds ?? [];
3664+
const excludedRecipientIds = proto.excludedRecipientIds ?? [];
3665+
3666+
const chatFolder: ChatFolder = {
3667+
id,
3668+
name: proto.name ?? '',
3669+
folderType,
3670+
showOnlyUnread: proto.showOnlyUnread ?? false,
3671+
showMutedChats: proto.showMutedChats ?? false,
3672+
includeAllIndividualChats: proto.includeAllIndividualChats ?? false,
3673+
includeAllGroupChats: proto.includeAllGroupChats ?? false,
3674+
includedConversationIds: includedRecipientIds.map(recipientId => {
3675+
const convo = this.#recipientIdToConvo.get(recipientId.toNumber());
3676+
strictAssert(convo != null, 'Missing chat folder included recipient');
3677+
return convo.id;
3678+
}),
3679+
excludedConversationIds: excludedRecipientIds.map(recipientId => {
3680+
const convo = this.#recipientIdToConvo.get(recipientId.toNumber());
3681+
strictAssert(convo != null, 'Missing chat folder included recipient');
3682+
return convo.id;
3683+
}),
3684+
position,
3685+
deletedAtTimestampMs: 0,
3686+
storageID: null,
3687+
storageVersion: null,
3688+
storageUnknownFields: null,
3689+
storageNeedsSync: false,
3690+
};
3691+
3692+
if (folderType === ChatFolderType.ALL) {
3693+
await DataWriter.upsertAllChatsChatFolderFromSync(chatFolder);
3694+
} else {
3695+
await DataWriter.createChatFolder(chatFolder);
3696+
}
3697+
}
3698+
36373699
async #fromCustomChatColors(
36383700
customChatColors:
36393701
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>

ts/services/backups/index.preload.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { measureSize } from '../../AttachmentCrypto.node.js';
5555
import { signalProtocolStore } from '../../SignalProtocolStore.preload.js';
5656
import { isTestOrMockEnvironment } from '../../environment.std.js';
5757
import { runStorageServiceSyncJob } from '../storage.preload.js';
58-
import { BackupExportStream, type StatsType } from './export.preload.js';
58+
import { BackupExportStream } from './export.preload.js';
5959
import { BackupImportStream } from './import.preload.js';
6060
import {
6161
getBackupId,
@@ -69,7 +69,12 @@ import {
6969
validateBackupStream,
7070
ValidationType,
7171
} from './validator.preload.js';
72-
import type { BackupExportOptions, BackupImportOptions } from './types.std.js';
72+
import type {
73+
BackupExportOptions,
74+
BackupImportOptions,
75+
ExportResultType,
76+
LocalBackupExportResultType,
77+
} from './types.std.js';
7378
import {
7479
BackupInstallerError,
7580
BackupDownloadFailedError,
@@ -126,16 +131,6 @@ type DoDownloadOptionsType = Readonly<{
126131
) => void;
127132
}>;
128133

129-
export type ExportResultType = Readonly<{
130-
totalBytes: number;
131-
duration: number;
132-
stats: Readonly<StatsType>;
133-
}>;
134-
135-
export type LocalBackupExportResultType = ExportResultType & {
136-
snapshotDir: string;
137-
};
138-
139134
export type ValidationResultType = Readonly<
140135
| {
141136
result: ExportResultType | LocalBackupExportResultType;

ts/services/backups/types.std.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,27 @@ export type LocalChatStyle = Readonly<{
5050
dimWallpaperInDarkMode: boolean | undefined;
5151
autoBubbleColor: boolean | undefined;
5252
}>;
53+
54+
export type StatsType = {
55+
adHocCalls: number;
56+
callLinks: number;
57+
conversations: number;
58+
chatFolders: number;
59+
chats: number;
60+
distributionLists: number;
61+
messages: number;
62+
notificationProfiles: number;
63+
skippedMessages: number;
64+
stickerPacks: number;
65+
fixedDirectMessages: number;
66+
};
67+
68+
export type ExportResultType = Readonly<{
69+
totalBytes: number;
70+
duration: number;
71+
stats: Readonly<StatsType>;
72+
}>;
73+
74+
export type LocalBackupExportResultType = ExportResultType & {
75+
snapshotDir: string;
76+
};

ts/test-electron/backup/integration_test.preload.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ describe('backup/integration', () => {
8080

8181
if (
8282
expectedString.includes('ReleaseChannelDonationRequest') ||
83-
// TODO (DESKTOP-8025) roundtrip these frames
84-
fullPath.includes('chat_folder') ||
8583
// TODO (DESKTOP-9209) roundtrip these frames when feature is added
8684
fullPath.includes('poll_terminate')
8785
) {

0 commit comments

Comments
 (0)