Setup pinned messages types and table

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Jamie
2025-11-19 11:12:29 -08:00
committed by GitHub
parent 710a54d43f
commit 7c12a1d3de
8 changed files with 206 additions and 2 deletions

View File

@@ -390,6 +390,20 @@ message DataMessage {
optional uint32 voteCount = 4;
}
message PinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
oneof pinDuration {
uint32 seconds = 3;
bool forever = 4;
}
}
message UnpinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
}
optional string body = 1;
repeated AttachmentPointer attachments = 2;
reserved /*groupV1*/ 3;
@@ -415,7 +429,9 @@ message DataMessage {
optional PollCreate pollCreate = 24;
optional PollTerminate pollTerminate = 25;
optional PollVote pollVote = 26;
// NEXT ID: 27
optional PinMessage pinMessage = 27;
optional UnpinMessage unpinMessage = 28;
// NEXT ID: 29
}
message NullMessage {

View File

@@ -65,6 +65,11 @@ import type { DonationReceipt } from '../types/Donations.std.js';
import type { InsertOrUpdateCallLinkFromSyncResult } from './server/callLinks.node.js';
import type { ChatFolderId, ChatFolder } from '../types/ChatFolder.std.js';
import type { CurrentChatFolder } from '../types/CurrentChatFolders.std.js';
import type {
PinnedMessage,
PinnedMessageId,
PinnedMessageParams,
} from '../types/PinnedMessage.std.js';
export type ReadableDB = Database & { __readable_db: never };
export type WritableDB = ReadableDB & { __writable_db: never };
@@ -965,6 +970,11 @@ type ReadableInterface = {
hasAllChatsChatFolder: () => boolean;
getOldestDeletedChatFolder: () => ChatFolder | null;
getPinnedMessagesForConversation: (
conversationId: string
) => ReadonlyArray<PinnedMessage>;
getNextExpiringPinnedMessageAcrossConversations: () => PinnedMessage | null;
getMessagesNeedingUpgrade: (
limit: number,
options: { maxVersion: number }
@@ -1324,6 +1334,14 @@ type WritableInterface = {
messageQueueTime: number
) => ReadonlyArray<ChatFolderId>;
createPinnedMessage: (
pinnedMessageParams: PinnedMessageParams
) => PinnedMessage;
deletePinnedMessage: (pinnedMessageId: PinnedMessageId) => void;
deleteAllExpiredPinnedMessagesBefore: (
beforeTimestamp: number
) => ReadonlyArray<PinnedMessageId>;
removeAll: () => void;
removeAllConfiguration: () => void;
eraseStorageServiceState: () => void;

View File

@@ -257,6 +257,13 @@ import {
updateChatFolderDeletedAtTimestampMsFromSync,
deleteExpiredChatFolders,
} from './server/chatFolders.std.js';
import {
getPinnedMessagesForConversation,
getNextExpiringPinnedMessageAcrossConversations,
createPinnedMessage,
deletePinnedMessage,
deleteAllExpiredPinnedMessagesBefore,
} from './server/pinnedMessages.std.js';
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer.std.js';
import type { GifType } from '../components/fun/panels/FunPanelGifs.dom.js';
import type { NotificationProfileType } from '../types/NotificationProfile.std.js';
@@ -471,6 +478,9 @@ export const DataReader: ServerReadableInterface = {
hasAllChatsChatFolder,
getOldestDeletedChatFolder,
getPinnedMessagesForConversation,
getNextExpiringPinnedMessageAcrossConversations,
callLinkExists,
defunctCallLinkExists,
getAllCallLinks,
@@ -725,6 +735,10 @@ export const DataWriter: ServerWritableInterface = {
markChatFolderDeleted,
deleteExpiredChatFolders,
createPinnedMessage,
deletePinnedMessage,
deleteAllExpiredPinnedMessagesBefore,
removeAll,
removeAllConfiguration,
eraseStorageServiceState,
@@ -8136,6 +8150,7 @@ function removeAll(db: WritableDB): void {
DELETE FROM messages_fts;
DELETE FROM messages;
DELETE FROM notificationProfiles;
DELETE FROM pinnedMessages;
DELETE FROM preKeys;
DELETE FROM reactions;
DELETE FROM recentGifs;

View File

@@ -3,7 +3,7 @@
import type { WritableDB } from '../Interface.std.js';
export default function updateToSchemaVersion1520(db: WritableDB): void {
export default function updateToSchemaVersion1550(db: WritableDB): void {
db.exec(`
ALTER TABLE messages
ADD COLUMN hasPreviews INTEGER NOT NULL

View File

@@ -0,0 +1,35 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { WritableDB } from '../Interface.std.js';
import { sql } from '../util.std.js';
export default function updateToSchemaVersion1560(db: WritableDB): void {
const [query] = sql`
CREATE TABLE pinnedMessages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversationId TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
messageId TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
messageSentAt INTEGER NOT NULL,
messageSenderAci TEXT NOT NULL,
pinnedByAci TEXT NOT NULL,
pinnedAt INTEGER NOT NULL,
expiresAt INTEGER,
UNIQUE (conversationId, messageId)
) STRICT;
CREATE INDEX pinnedMessages_byConversation
ON pinnedMessages(
conversationId,
pinnedAt DESC,
messageId
);
CREATE INDEX pinnedMessages_byExpiresAt
ON pinnedMessages(
expiresAt ASC
)
WHERE expiresAt IS NOT NULL;
`;
db.exec(query);
}

View File

@@ -131,6 +131,7 @@ import updateToSchemaVersion1520 from './1520-poll-votes-unread.std.js';
import updateToSchemaVersion1530 from './1530-update-expiring-index.std.js';
import updateToSchemaVersion1540 from './1540-partial-expiring-index.std.js';
import updateToSchemaVersion1550 from './1550-has-link-preview.std.js';
import updateToSchemaVersion1560 from './1560-pinned-messages.std.js';
import { DataWriter } from '../Server.node.js';
@@ -1620,6 +1621,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
{ version: 1530, update: updateToSchemaVersion1530 },
{ version: 1540, update: updateToSchemaVersion1540 },
{ version: 1550, update: updateToSchemaVersion1550 },
{ version: 1560, update: updateToSchemaVersion1560 },
];
export class DBVersionFromFutureError extends Error {

View File

@@ -0,0 +1,91 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type {
PinnedMessage,
PinnedMessageId,
PinnedMessageParams,
} from '../../types/PinnedMessage.std.js';
import { strictAssert } from '../../util/assert.std.js';
import type { ReadableDB, WritableDB } from '../Interface.std.js';
import { sql } from '../util.std.js';
export function getPinnedMessagesForConversation(
db: ReadableDB,
conversationId: string
): ReadonlyArray<PinnedMessage> {
const [query, params] = sql`
SELECT * FROM pins
WHERE conversationId = ${conversationId}
ORDER BY pinnedAt DESC
`;
return db.prepare(query).all<PinnedMessage>(params);
}
export function createPinnedMessage(
db: WritableDB,
pinnedMessageParams: PinnedMessageParams
): PinnedMessage {
const [query, params] = sql`
INSERT INTO pinnedMessages (
conversationId,
messageId,
messageSentAt,
messageSenderAci,
pinnedByAci,
pinnedAt,
expiresAt
) VALUES (
${pinnedMessageParams.conversationId},
${pinnedMessageParams.messageId},
${pinnedMessageParams.messageSentAt},
${pinnedMessageParams.messageSenderAci},
${pinnedMessageParams.pinnedByAci},
${pinnedMessageParams.pinnedAt},
${pinnedMessageParams.expiresAt}
)
RETURNING *;
`;
const row = db.prepare(query).get<PinnedMessage>(params);
strictAssert(row != null, 'createPinnedMessage: Failed to insert');
return row;
}
export function deletePinnedMessage(
db: WritableDB,
pinnedMessageId: PinnedMessageId
): void {
const [query, params] = sql`
DELETE FROM pinnedMessages
WHERE id = ${pinnedMessageId}
`;
const result = db.prepare(query).run(params);
strictAssert(
result.changes === 1,
`deletePinnedMessage: Expected changes: 1, Actual: ${result.changes}`
);
}
export function getNextExpiringPinnedMessageAcrossConversations(
db: ReadableDB
): PinnedMessage | null {
const [query, params] = sql`
SELECT * FROM pinnedMessages
ORDER BY expiresAt ASC
LIMIT 1
`;
return db.prepare(query).get<PinnedMessage>(params) ?? null;
}
export function deleteAllExpiredPinnedMessagesBefore(
db: WritableDB,
beforeTimestamp: number
): ReadonlyArray<PinnedMessageId> {
const [query, params] = sql`
DELETE FROM pinnedMessages
WHERE expiresAt <= ${beforeTimestamp}
RETURNING id
`;
return db.prepare(query, { pluck: true }).all<PinnedMessageId>(params);
}

View File

@@ -0,0 +1,27 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.js';
import type { ConversationType } from '../state/ducks/conversations.preload.js';
import type { AciString } from './ServiceId.std.js';
export type PinnedMessageId = number & { PinnedMessageId: never };
export type PinnedMessage = Readonly<{
id: PinnedMessageId;
conversationId: string;
messageId: string;
messageSentAt: number;
messageSenderAci: AciString;
pinnedByAci: AciString;
pinnedAt: number;
expiresAt: number | null;
}>;
export type PinnedMessageParams = Omit<PinnedMessage, 'id'>;
export type PinnedMessageRenderData = Readonly<{
pinnedMessage: PinnedMessage;
sender: ConversationType;
message: MessageAttributesType;
}>;