Add message schema version section to internal settings

This commit is contained in:
trevor-signal 2025-06-02 17:20:06 -04:00 committed by GitHub
commit 46bf933e72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 197 additions and 9 deletions

View file

@ -625,21 +625,41 @@ $secondary-text-color: light-dark(
margin-inline: 4px; margin-inline: 4px;
} }
} }
pre {
max-height: 128px;
}
} }
.Preferences--internal--validate-backup--result { .Preferences--internal--result {
padding-inline: 48px 24px; 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; padding-inline: 48px 24px;
color: variables.$color-accent-red; color: variables.$color-accent-red;
} }
.Preferences--internal--validate-backup--result pre, .Preferences--internal pre,
.Preferences--internal--validate-backup--error pre { .Preferences--internal pre {
max-height: 128px; max-height: 400px;
max-width: 100%; max-width: 100%;
white-space: pre-wrap; white-space: pre-wrap;
user-select: text; user-select: text;
overflow-x: scroll;
} }

View file

@ -16,6 +16,7 @@ import { DialogType } from '../types/Dialogs';
import type { PropsType } from './Preferences'; import type { PropsType } from './Preferences';
import type { WidthBreakpoint } from './_util'; import type { WidthBreakpoint } from './_util';
import type { MessageAttributesType } from '../model-types';
const { i18n } = window.SignalContext; const { i18n } = window.SignalContext;
@ -200,6 +201,13 @@ export default {
result: exportLocalBackupResult, result: exportLocalBackupResult,
}; };
}, },
getMessageCountBySchemaVersion: async () => [
{ schemaVersion: 10, count: 1024 },
{ schemaVersion: 8, count: 256 },
],
getMessageSampleForSchemaVersion: async () => [
{ id: 'messageId' } as MessageAttributesType,
],
makeSyncRequest: action('makeSyncRequest'), makeSyncRequest: action('makeSyncRequest'),
onAudioNotificationsChange: action('onAudioNotificationsChange'), onAudioNotificationsChange: action('onAudioNotificationsChange'),
onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'), onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'),

View file

@ -75,6 +75,8 @@ import { PreferencesInternal } from './PreferencesInternal';
import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider'; import { FunEmojiLocalizationProvider } from './fun/FunEmojiLocalizationProvider';
import { NavTabsToggle } from './NavTabs'; import { NavTabsToggle } from './NavTabs';
import type { UnreadStats } from '../util/countUnreadStats'; import type { UnreadStats } from '../util/countUnreadStats';
import type { MessageCountBySchemaVersionType } from '../sql/Interface';
import type { MessageAttributesType } from '../model-types';
type CheckboxChangeHandlerType = (value: boolean) => unknown; type CheckboxChangeHandlerType = (value: boolean) => unknown;
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown; type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
@ -171,6 +173,10 @@ type PropsFunctionType = {
doDeleteAllData: () => unknown; doDeleteAllData: () => unknown;
editCustomColor: (colorId: string, color: CustomColorType) => unknown; editCustomColor: (colorId: string, color: CustomColorType) => unknown;
exportLocalBackup: () => Promise<BackupValidationResultType>; exportLocalBackup: () => Promise<BackupValidationResultType>;
getMessageCountBySchemaVersion: () => Promise<MessageCountBySchemaVersionType>;
getMessageSampleForSchemaVersion: (
version: number
) => Promise<Array<MessageAttributesType>>;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>; getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
makeSyncRequest: () => unknown; makeSyncRequest: () => unknown;
onStartUpdate: () => unknown; onStartUpdate: () => unknown;
@ -301,6 +307,8 @@ export function Preferences({
emojiSkinToneDefault, emojiSkinToneDefault,
exportLocalBackup, exportLocalBackup,
getConversationsWithCustomColor, getConversationsWithCustomColor,
getMessageCountBySchemaVersion,
getMessageSampleForSchemaVersion,
hasAudioNotifications, hasAudioNotifications,
hasAutoConvertEmoji, hasAutoConvertEmoji,
hasAutoDownloadUpdate, hasAutoDownloadUpdate,
@ -1800,6 +1808,8 @@ export function Preferences({
i18n={i18n} i18n={i18n}
exportLocalBackup={exportLocalBackup} exportLocalBackup={exportLocalBackup}
validateBackup={validateBackup} validateBackup={validateBackup}
getMessageCountBySchemaVersion={getMessageCountBySchemaVersion}
getMessageSampleForSchemaVersion={getMessageSampleForSchemaVersion}
/> />
); );
} }

View file

@ -10,21 +10,35 @@ import type { ValidationResultType as BackupValidationResultType } from '../serv
import { SettingsRow, SettingsControl } from './PreferencesUtil'; import { SettingsRow, SettingsControl } from './PreferencesUtil';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import type { MessageCountBySchemaVersionType } from '../sql/Interface';
import type { MessageAttributesType } from '../model-types';
export function PreferencesInternal({ export function PreferencesInternal({
i18n, i18n,
exportLocalBackup: doExportLocalBackup, exportLocalBackup: doExportLocalBackup,
validateBackup: doValidateBackup, validateBackup: doValidateBackup,
getMessageCountBySchemaVersion,
getMessageSampleForSchemaVersion,
}: { }: {
i18n: LocalizerType; i18n: LocalizerType;
exportLocalBackup: () => Promise<BackupValidationResultType>; exportLocalBackup: () => Promise<BackupValidationResultType>;
validateBackup: () => Promise<BackupValidationResultType>; validateBackup: () => Promise<BackupValidationResultType>;
getMessageCountBySchemaVersion: () => Promise<MessageCountBySchemaVersionType>;
getMessageSampleForSchemaVersion: (
version: number
) => Promise<Array<MessageAttributesType>>;
}): JSX.Element { }): JSX.Element {
const [isExportPending, setIsExportPending] = useState(false); const [isExportPending, setIsExportPending] = useState(false);
const [exportResult, setExportResult] = useState< const [exportResult, setExportResult] = useState<
BackupValidationResultType | undefined BackupValidationResultType | undefined
>(); >();
const [messageCountBySchemaVersion, setMessageCountBySchemaVersion] =
useState<MessageCountBySchemaVersionType>();
const [messageSampleForVersions, setMessageSampleForVersions] = useState<{
[schemaVersion: number]: Array<MessageAttributesType>;
}>();
const [isValidationPending, setIsValidationPending] = useState(false); const [isValidationPending, setIsValidationPending] = useState(false);
const [validationResult, setValidationResult] = useState< const [validationResult, setValidationResult] = useState<
BackupValidationResultType | undefined BackupValidationResultType | undefined
@ -68,7 +82,7 @@ export function PreferencesInternal({
} }
return ( return (
<div className="Preferences--internal--validate-backup--result"> <div className="Preferences--internal--result">
{snapshotDirEl} {snapshotDirEl}
<p>Main file size: {formatFileSize(totalBytes)}</p> <p>Main file size: {formatFileSize(totalBytes)}</p>
<p>Duration: {Math.round(duration / SECOND)}s</p> <p>Duration: {Math.round(duration / SECOND)}s</p>
@ -82,7 +96,7 @@ export function PreferencesInternal({
const { error } = backupResult; const { error } = backupResult;
return ( return (
<div className="Preferences--internal--validate-backup--error"> <div className="Preferences--internal--error">
<pre> <pre>
<code>{error}</code> <code>{error}</code>
</pre> </pre>
@ -105,7 +119,7 @@ export function PreferencesInternal({
}, [doExportLocalBackup]); }, [doExportLocalBackup]);
return ( return (
<> <div className="Preferences--internal">
<SettingsRow <SettingsRow
className="Preferences--internal--backups" className="Preferences--internal--backups"
title={i18n('icu:Preferences__button--backups')} title={i18n('icu:Preferences__button--backups')}
@ -155,6 +169,91 @@ export function PreferencesInternal({
{renderValidationResult(exportResult)} {renderValidationResult(exportResult)}
</SettingsRow> </SettingsRow>
</>
<SettingsRow
className="Preferences--internal--message-schemas"
title="Message schema versions"
>
<SettingsControl
left="Check message schema versions"
right={
<Button
variant={ButtonVariant.Secondary}
onClick={async () => {
setMessageCountBySchemaVersion(
await getMessageCountBySchemaVersion()
);
setMessageSampleForVersions({});
}}
disabled={isExportPending}
>
Fetch data
</Button>
}
/>
{messageCountBySchemaVersion ? (
<div className="Preferences--internal--result">
<pre>
<table>
<thead>
<tr>
<th>Schema version</th>
<th># Messages</th>
</tr>
</thead>
<tbody>
{messageCountBySchemaVersion.map(
({ schemaVersion, count }) => {
return (
<React.Fragment key={schemaVersion}>
<tr>
<td>{schemaVersion}</td>
<td>{count}</td>
<td>
<button
type="button"
onClick={async () => {
const sampleMessages =
await getMessageSampleForSchemaVersion(
schemaVersion
);
setMessageSampleForVersions({
[schemaVersion]: sampleMessages,
});
}}
disabled={isExportPending}
>
Sample
</button>
</td>
</tr>
{messageSampleForVersions?.[schemaVersion] ? (
<tr
key={`${schemaVersion}_samples`}
className="Preferences--internal--subresult"
>
<td colSpan={3}>
<code>
{JSON.stringify(
messageSampleForVersions[schemaVersion],
null,
2
)}
</code>
</td>
</tr>
) : null}
</React.Fragment>
);
}
)}
</tbody>
</table>
</pre>
</div>
) : null}
</SettingsRow>
</div>
); );
} }

View file

@ -548,6 +548,11 @@ export enum AttachmentDownloadSource {
BACKFILL = 'backfill', BACKFILL = 'backfill',
} }
export type MessageCountBySchemaVersionType = Array<{
schemaVersion: number;
count: number;
}>;
export const MESSAGE_ATTACHMENT_COLUMNS = [ export const MESSAGE_ATTACHMENT_COLUMNS = [
'messageId', 'messageId',
'conversationId', 'conversationId',
@ -888,6 +893,11 @@ type ReadableInterface = {
getAttachmentReferencesForMessages: ( getAttachmentReferencesForMessages: (
messageIds: Array<string> messageIds: Array<string>
) => Array<MessageAttachmentDBType>; ) => Array<MessageAttachmentDBType>;
getMessageCountBySchemaVersion: () => MessageCountBySchemaVersionType;
getMessageSampleForSchemaVersion: (
version: number
) => Array<MessageAttributesType>;
}; };
type WritableInterface = { type WritableInterface = {

View file

@ -186,6 +186,7 @@ import type {
MessageAttachmentDBType, MessageAttachmentDBType,
MessageTypeUnhydrated, MessageTypeUnhydrated,
ServerMessageSearchResultType, ServerMessageSearchResultType,
MessageCountBySchemaVersionType,
} from './Interface'; } from './Interface';
import { import {
AttachmentDownloadSource, AttachmentDownloadSource,
@ -436,6 +437,8 @@ export const DataReader: ServerReadableInterface = {
getBackupCdnObjectMetadata, getBackupCdnObjectMetadata,
getSizeOfPendingBackupAttachmentDownloadJobs, getSizeOfPendingBackupAttachmentDownloadJobs,
getAttachmentReferencesForMessages, getAttachmentReferencesForMessages,
getMessageCountBySchemaVersion,
getMessageSampleForSchemaVersion,
// Server-only // Server-only
getKnownMessageAttachments, 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<MessageAttributesType> {
return db.transaction(() => {
const [query, params] = sql`
SELECT * from messages
WHERE schemaVersion = ${version}
ORDER BY RANDOM()
LIMIT 2;
`;
const rows = db.prepare(query).all<MessageTypeUnhydrated>(params);
return hydrateMessages(db, rows);
})();
}
function disableMessageInsertTriggers(db: WritableDB): void { function disableMessageInsertTriggers(db: WritableDB): void {
db.transaction(() => { db.transaction(() => {
createOrUpdateItem(db, { createOrUpdateItem(db, {

View file

@ -55,6 +55,7 @@ import {
} from '../selectors/updates'; } from '../selectors/updates';
import { getHasAnyFailedStorySends } from '../selectors/stories'; import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getOtherTabsUnreadStats } from '../selectors/nav'; import { getOtherTabsUnreadStats } from '../selectors/nav';
import { DataReader } from '../../sql/Client';
const DEFAULT_NOTIFICATION_SETTING = 'message'; const DEFAULT_NOTIFICATION_SETTING = 'message';
@ -604,6 +605,12 @@ export function SmartPreferences(): JSX.Element {
doDeleteAllData={doDeleteAllData} doDeleteAllData={doDeleteAllData}
editCustomColor={editCustomColor} editCustomColor={editCustomColor}
getConversationsWithCustomColor={getConversationsWithCustomColor} getConversationsWithCustomColor={getConversationsWithCustomColor}
getMessageCountBySchemaVersion={
DataReader.getMessageCountBySchemaVersion
}
getMessageSampleForSchemaVersion={
DataReader.getMessageSampleForSchemaVersion
}
hasAudioNotifications={hasAudioNotifications} hasAudioNotifications={hasAudioNotifications}
hasAutoConvertEmoji={hasAutoConvertEmoji} hasAutoConvertEmoji={hasAutoConvertEmoji}
hasAutoDownloadUpdate={hasAutoDownloadUpdate} hasAutoDownloadUpdate={hasAutoDownloadUpdate}