From dc95fd9664fffcea27edf339927891e45d6c22e1 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:30:28 -0600 Subject: [PATCH] Add migration for unread call history messages and fix json.seenStatus Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- ts/sql/Server.ts | 17 +- ...-unread-call-history-messages-as-unseen.ts | 60 ++++++ ts/sql/migrations/index.ts | 6 +- ts/test-node/sql/migration_1000_test.ts | 189 ++++++++++++++++++ 4 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts create mode 100644 ts/test-node/sql/migration_1000_test.ts diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 5ca33cffcc..8d8f894f7f 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -3471,9 +3471,16 @@ async function getCallHistoryUnreadCount(): Promise { async function markCallHistoryRead(callId: string): Promise { const db = await getWritableInstance(); + + const jsonPatch = JSON.stringify({ + seenStatus: SeenStatus.Seen, + }); + const [query, params] = sql` UPDATE messages - SET seenStatus = ${SEEN_STATUS_UNSEEN} + SET + seenStatus = ${SEEN_STATUS_UNSEEN} + json = json_patch(json, ${jsonPatch}) WHERE type IS 'call-history' AND callId IS ${callId} `; @@ -3497,9 +3504,15 @@ async function markAllCallHistoryRead(): Promise> { const conversationIds = db.prepare(selectQuery).pluck().all(selectParams); + const jsonPatch = JSON.stringify({ + seenStatus: SeenStatus.Seen, + }); + const [updateQuery, updateParams] = sql` UPDATE messages - SET seenStatus = ${SEEN_STATUS_SEEN} + SET + seenStatus = ${SEEN_STATUS_SEEN}, + json = json_patch(json, ${jsonPatch}) ${where}; `; diff --git a/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts b/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts new file mode 100644 index 0000000000..f9bef5a5f4 --- /dev/null +++ b/ts/sql/migrations/1000-mark-unread-call-history-messages-as-unseen.ts @@ -0,0 +1,60 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Database } from '@signalapp/better-sqlite3'; + +import type { LoggerType } from '../../types/Logging'; +import { ReadStatus } from '../../messages/MessageReadStatus'; +import { SeenStatus } from '../../MessageSeenStatus'; +import { strictAssert } from '../../util/assert'; +import { sql, sqlConstant } from '../util'; + +export const version = 1000; + +const READ_STATUS_UNREAD = sqlConstant(ReadStatus.Unread); +const READ_STATUS_READ = sqlConstant(ReadStatus.Read); +const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen); + +export function updateToSchemaVersion1000( + currentVersion: number, + db: Database, + logger: LoggerType +): void { + if (currentVersion >= 1000) { + return; + } + + db.transaction(() => { + const [selectQuery] = sql` + SELECT id + FROM messages + WHERE messages.type = 'call-history' + AND messages.readStatus IS ${READ_STATUS_UNREAD} + `; + + const rows = db.prepare(selectQuery).all(); + + for (const row of rows) { + const { id } = row; + strictAssert(id != null, 'message id must exist'); + + const [updateQuery, updateParams] = sql` + UPDATE messages + SET + json = JSON_PATCH(json, ${JSON.stringify({ + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Unseen, + })}), + readStatus = ${READ_STATUS_READ}, + seenStatus = ${SEEN_STATUS_UNSEEN} + WHERE id = ${id} + `; + + db.prepare(updateQuery).run(updateParams); + } + })(); + + db.pragma('user_version = 1000'); + + logger.info('updateToSchemaVersion1000: success!'); +} diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index a106fe4548..199b8163f3 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -74,10 +74,11 @@ import { updateToSchemaVersion950 } from './950-fts5-secure-delete'; import { updateToSchemaVersion960 } from './960-untag-pni'; import { updateToSchemaVersion970 } from './970-fts5-optimize'; import { updateToSchemaVersion980 } from './980-reaction-timestamp'; +import { updateToSchemaVersion990 } from './990-phone-number-sharing'; import { version as MAX_VERSION, - updateToSchemaVersion990, -} from './990-phone-number-sharing'; + updateToSchemaVersion1000, +} from './1000-mark-unread-call-history-messages-as-unseen'; function updateToSchemaVersion1( currentVersion: number, @@ -2019,6 +2020,7 @@ export const SCHEMA_VERSIONS = [ updateToSchemaVersion970, updateToSchemaVersion980, updateToSchemaVersion990, + updateToSchemaVersion1000, ]; export class DBVersionFromFutureError extends Error { diff --git a/ts/test-node/sql/migration_1000_test.ts b/ts/test-node/sql/migration_1000_test.ts new file mode 100644 index 0000000000..84f49ff589 --- /dev/null +++ b/ts/test-node/sql/migration_1000_test.ts @@ -0,0 +1,189 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import type { Database } from '@signalapp/better-sqlite3'; +import SQL from '@signalapp/better-sqlite3'; +import { v4 as generateGuid } from 'uuid'; + +import { jsonToObject, sql } from '../../sql/util'; +import { updateToVersion } from './helpers'; +import type { MessageType } from '../../sql/Interface'; +import { ReadStatus } from '../../messages/MessageReadStatus'; +import { SeenStatus } from '../../MessageSeenStatus'; + +describe('SQL/updateToSchemaVersion1000', () => { + let db: Database; + + beforeEach(() => { + db = new SQL(':memory:'); + updateToVersion(db, 990); + }); + + afterEach(() => { + db.close(); + }); + + function createCallHistoryMessage(options: { + messageId: string; + conversationId: string; + callId: string; + readStatus: ReadStatus; + seenStatus: SeenStatus; + }): MessageType { + const message: MessageType = { + id: options.messageId, + type: 'call-history', + conversationId: options.conversationId, + received_at: Date.now(), + sent_at: Date.now(), + received_at_ms: Date.now(), + timestamp: Date.now(), + readStatus: options.readStatus, + seenStatus: options.seenStatus, + callId: options.callId, + }; + + const json = JSON.stringify(message); + + const [query, params] = sql` + INSERT INTO messages + (id, conversationId, type, readStatus, seenStatus, json) + VALUES + ( + ${message.id}, + ${message.conversationId}, + ${message.type}, + ${message.readStatus}, + ${message.seenStatus}, + ${json} + ) + `; + + db.prepare(query).run(params); + + return message; + } + + function createConversation( + type: 'private' | 'group', + discoveredUnregisteredAt?: number + ) { + const id = generateGuid(); + const groupId = type === 'group' ? generateGuid() : null; + + const json = JSON.stringify({ + type, + id, + groupId, + discoveredUnregisteredAt, + }); + + const [query, params] = sql` + INSERT INTO conversations + (id, type, groupId, json) + VALUES + (${id}, ${type}, ${groupId}, ${json}); + `; + + db.prepare(query).run(params); + + return { id, groupId }; + } + + function getMessages() { + const [query] = sql` + SELECT json, readStatus, seenStatus FROM messages; + `; + return db + .prepare(query) + .all() + .map(row => { + return { + message: jsonToObject(row.json), + readStatus: row.readStatus, + seenStatus: row.seenStatus, + }; + }); + } + + it('marks unread call history messages read and unseen', () => { + const conversation1 = createConversation('private'); + const conversation2 = createConversation('group'); + + const callId1 = '1'; + const callId2 = '2'; + + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation1.id, + callId: callId1, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, + }); + + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation2.id, + callId: callId2, + readStatus: ReadStatus.Unread, + seenStatus: SeenStatus.Unseen, + }); + + updateToVersion(db, 1000); + + const messages = getMessages(); + + assert.strictEqual(messages.length, 2); + + assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Unseen); + assert.strictEqual(messages[0].readStatus, ReadStatus.Read); + assert.strictEqual(messages[0].seenStatus, SeenStatus.Unseen); + + assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Unseen); + assert.strictEqual(messages[1].readStatus, ReadStatus.Read); + assert.strictEqual(messages[1].seenStatus, SeenStatus.Unseen); + }); + + it('does not mark read call history messages as unseen', () => { + const conversation1 = createConversation('private'); + const conversation2 = createConversation('group'); + + const callId1 = '1'; + const callId2 = '2'; + + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation1.id, + callId: callId1, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + }); + + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation2.id, + callId: callId2, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + }); + + updateToVersion(db, 1000); + + const messages = getMessages(); + + assert.strictEqual(messages.length, 2); + + assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Seen); + assert.strictEqual(messages[0].readStatus, ReadStatus.Read); + assert.strictEqual(messages[0].seenStatus, SeenStatus.Seen); + + assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Seen); + assert.strictEqual(messages[1].readStatus, ReadStatus.Read); + assert.strictEqual(messages[1].seenStatus, SeenStatus.Seen); + }); +});