diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss index 0d732899a9e..ef286677759 100644 --- a/stylesheets/components/Preferences.scss +++ b/stylesheets/components/Preferences.scss @@ -625,21 +625,41 @@ $secondary-text-color: light-dark( margin-inline: 4px; } } + pre { + max-height: 128px; + } } -.Preferences--internal--validate-backup--result { +.Preferences--internal--result { padding-inline: 48px 24px; + max-width: 100%; + + table { + width: 100%; + } + th, + td { + padding-inline: 16px; + padding-block: 4px; + text-align: start; + max-width: 600px; + } + .Preferences--internal--subresult { + background-color: variables.$color-white-alpha-06; + font-size: 0.8em; + } } -.Preferences--internal--validate-backup--error { +.Preferences--internal--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; +.Preferences--internal pre, +.Preferences--internal pre { + max-height: 400px; max-width: 100%; white-space: pre-wrap; user-select: text; + overflow-x: scroll; } diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index 51d1282a7b8..5f5f4d3726b 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -16,6 +16,7 @@ import { DialogType } from '../types/Dialogs'; import type { PropsType } from './Preferences'; import type { WidthBreakpoint } from './_util'; +import type { MessageAttributesType } from '../model-types'; const { i18n } = window.SignalContext; @@ -200,6 +201,13 @@ export default { result: exportLocalBackupResult, }; }, + getMessageCountBySchemaVersion: async () => [ + { schemaVersion: 10, count: 1024 }, + { schemaVersion: 8, count: 256 }, + ], + getMessageSampleForSchemaVersion: async () => [ + { id: 'messageId' } as MessageAttributesType, + ], makeSyncRequest: action('makeSyncRequest'), onAudioNotificationsChange: action('onAudioNotificationsChange'), onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'), diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index e175c18a5c5..0a7b7d4a52a 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -75,6 +75,8 @@ import { PreferencesInternal } from './PreferencesInternal'; import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider'; import { NavTabsToggle } from './NavTabs'; import type { UnreadStats } from '../util/countUnreadStats'; +import type { MessageCountBySchemaVersionType } from '../sql/Interface'; +import type { MessageAttributesType } from '../model-types'; type CheckboxChangeHandlerType = (value: boolean) => unknown; type SelectChangeHandlerType = (value: T) => unknown; @@ -171,6 +173,10 @@ type PropsFunctionType = { doDeleteAllData: () => unknown; editCustomColor: (colorId: string, color: CustomColorType) => unknown; exportLocalBackup: () => Promise; + getMessageCountBySchemaVersion: () => Promise; + getMessageSampleForSchemaVersion: ( + version: number + ) => Promise>; getConversationsWithCustomColor: (colorId: string) => Array; makeSyncRequest: () => unknown; onStartUpdate: () => unknown; @@ -301,6 +307,8 @@ export function Preferences({ emojiSkinToneDefault, exportLocalBackup, getConversationsWithCustomColor, + getMessageCountBySchemaVersion, + getMessageSampleForSchemaVersion, hasAudioNotifications, hasAutoConvertEmoji, hasAutoDownloadUpdate, @@ -1800,6 +1808,8 @@ export function Preferences({ i18n={i18n} exportLocalBackup={exportLocalBackup} validateBackup={validateBackup} + getMessageCountBySchemaVersion={getMessageCountBySchemaVersion} + getMessageSampleForSchemaVersion={getMessageSampleForSchemaVersion} /> ); } diff --git a/ts/components/PreferencesInternal.tsx b/ts/components/PreferencesInternal.tsx index f5044b17d7a..c7bda77dd72 100644 --- a/ts/components/PreferencesInternal.tsx +++ b/ts/components/PreferencesInternal.tsx @@ -10,21 +10,35 @@ import type { ValidationResultType as BackupValidationResultType } from '../serv import { SettingsRow, SettingsControl } from './PreferencesUtil'; import { Button, ButtonVariant } from './Button'; import { Spinner } from './Spinner'; +import type { MessageCountBySchemaVersionType } from '../sql/Interface'; +import type { MessageAttributesType } from '../model-types'; export function PreferencesInternal({ i18n, exportLocalBackup: doExportLocalBackup, validateBackup: doValidateBackup, + getMessageCountBySchemaVersion, + getMessageSampleForSchemaVersion, }: { i18n: LocalizerType; exportLocalBackup: () => Promise; validateBackup: () => Promise; + getMessageCountBySchemaVersion: () => Promise; + getMessageSampleForSchemaVersion: ( + version: number + ) => Promise>; }): JSX.Element { const [isExportPending, setIsExportPending] = useState(false); const [exportResult, setExportResult] = useState< BackupValidationResultType | undefined >(); + const [messageCountBySchemaVersion, setMessageCountBySchemaVersion] = + useState(); + const [messageSampleForVersions, setMessageSampleForVersions] = useState<{ + [schemaVersion: number]: Array; + }>(); + const [isValidationPending, setIsValidationPending] = useState(false); const [validationResult, setValidationResult] = useState< BackupValidationResultType | undefined @@ -68,7 +82,7 @@ export function PreferencesInternal({ } return ( -
+
{snapshotDirEl}

Main file size: {formatFileSize(totalBytes)}

Duration: {Math.round(duration / SECOND)}s

@@ -82,7 +96,7 @@ export function PreferencesInternal({ const { error } = backupResult; return ( -
+
             {error}
           
@@ -105,7 +119,7 @@ export function PreferencesInternal({ }, [doExportLocalBackup]); return ( - <> +
- + + + { + setMessageCountBySchemaVersion( + await getMessageCountBySchemaVersion() + ); + setMessageSampleForVersions({}); + }} + disabled={isExportPending} + > + Fetch data + + } + /> + + {messageCountBySchemaVersion ? ( +
+
+              
+                
+                  
+                    
+                    
+                  
+                
+                
+                  {messageCountBySchemaVersion.map(
+                    ({ schemaVersion, count }) => {
+                      return (
+                        
+                          
+                            
+                            
+                            
+                          
+                          {messageSampleForVersions?.[schemaVersion] ? (
+                            
+                              
+                            
+                          ) : null}
+                        
+                      );
+                    }
+                  )}
+                
+              
Schema version# Messages
{schemaVersion}{count} + +
+ + {JSON.stringify( + messageSampleForVersions[schemaVersion], + null, + 2 + )} + +
+
+
+ ) : null} +
+
); } diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index e3baa55de6a..01de697ce8e 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -548,6 +548,11 @@ export enum AttachmentDownloadSource { BACKFILL = 'backfill', } +export type MessageCountBySchemaVersionType = Array<{ + schemaVersion: number; + count: number; +}>; + export const MESSAGE_ATTACHMENT_COLUMNS = [ 'messageId', 'conversationId', @@ -888,6 +893,11 @@ type ReadableInterface = { getAttachmentReferencesForMessages: ( messageIds: Array ) => Array; + + getMessageCountBySchemaVersion: () => MessageCountBySchemaVersionType; + getMessageSampleForSchemaVersion: ( + version: number + ) => Array; }; type WritableInterface = { diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 910a6e43712..078201ec260 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -186,6 +186,7 @@ import type { MessageAttachmentDBType, MessageTypeUnhydrated, ServerMessageSearchResultType, + MessageCountBySchemaVersionType, } from './Interface'; import { AttachmentDownloadSource, @@ -436,6 +437,8 @@ export const DataReader: ServerReadableInterface = { getBackupCdnObjectMetadata, getSizeOfPendingBackupAttachmentDownloadJobs, getAttachmentReferencesForMessages, + getMessageCountBySchemaVersion, + getMessageSampleForSchemaVersion, // Server-only getKnownMessageAttachments, @@ -8463,6 +8466,37 @@ function getUnreadEditedMessagesAndMarkRead( })(); } +function getMessageCountBySchemaVersion( + db: ReadableDB +): MessageCountBySchemaVersionType { + const [query, params] = sql` + SELECT schemaVersion, COUNT(1) as count from messages + GROUP BY schemaVersion; + `; + const rows = db + .prepare(query) + .all<{ schemaVersion: number; count: number }>(params); + + return rows.sort((a, b) => a.schemaVersion - b.schemaVersion); +} + +function getMessageSampleForSchemaVersion( + db: ReadableDB, + version: number +): Array { + return db.transaction(() => { + const [query, params] = sql` + SELECT * from messages + WHERE schemaVersion = ${version} + ORDER BY RANDOM() + LIMIT 2; + `; + const rows = db.prepare(query).all(params); + + return hydrateMessages(db, rows); + })(); +} + function disableMessageInsertTriggers(db: WritableDB): void { db.transaction(() => { createOrUpdateItem(db, { diff --git a/ts/state/smart/Preferences.tsx b/ts/state/smart/Preferences.tsx index 469a93a3008..731bd69b51e 100644 --- a/ts/state/smart/Preferences.tsx +++ b/ts/state/smart/Preferences.tsx @@ -55,6 +55,7 @@ import { } from '../selectors/updates'; import { getHasAnyFailedStorySends } from '../selectors/stories'; import { getOtherTabsUnreadStats } from '../selectors/nav'; +import { DataReader } from '../../sql/Client'; const DEFAULT_NOTIFICATION_SETTING = 'message'; @@ -604,6 +605,12 @@ export function SmartPreferences(): JSX.Element { doDeleteAllData={doDeleteAllData} editCustomColor={editCustomColor} getConversationsWithCustomColor={getConversationsWithCustomColor} + getMessageCountBySchemaVersion={ + DataReader.getMessageCountBySchemaVersion + } + getMessageSampleForSchemaVersion={ + DataReader.getMessageSampleForSchemaVersion + } hasAudioNotifications={hasAudioNotifications} hasAutoConvertEmoji={hasAutoConvertEmoji} hasAutoDownloadUpdate={hasAutoDownloadUpdate}