mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Setup pinned messages types and table
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
35
ts/sql/migrations/1560-pinned-messages.std.ts
Normal file
35
ts/sql/migrations/1560-pinned-messages.std.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
91
ts/sql/server/pinnedMessages.std.ts
Normal file
91
ts/sql/server/pinnedMessages.std.ts
Normal 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);
|
||||
}
|
||||
27
ts/types/PinnedMessage.std.ts
Normal file
27
ts/types/PinnedMessage.std.ts
Normal 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;
|
||||
}>;
|
||||
Reference in New Issue
Block a user