mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +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;
|
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;
|
optional string body = 1;
|
||||||
repeated AttachmentPointer attachments = 2;
|
repeated AttachmentPointer attachments = 2;
|
||||||
reserved /*groupV1*/ 3;
|
reserved /*groupV1*/ 3;
|
||||||
@@ -415,7 +429,9 @@ message DataMessage {
|
|||||||
optional PollCreate pollCreate = 24;
|
optional PollCreate pollCreate = 24;
|
||||||
optional PollTerminate pollTerminate = 25;
|
optional PollTerminate pollTerminate = 25;
|
||||||
optional PollVote pollVote = 26;
|
optional PollVote pollVote = 26;
|
||||||
// NEXT ID: 27
|
optional PinMessage pinMessage = 27;
|
||||||
|
optional UnpinMessage unpinMessage = 28;
|
||||||
|
// NEXT ID: 29
|
||||||
}
|
}
|
||||||
|
|
||||||
message NullMessage {
|
message NullMessage {
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ import type { DonationReceipt } from '../types/Donations.std.js';
|
|||||||
import type { InsertOrUpdateCallLinkFromSyncResult } from './server/callLinks.node.js';
|
import type { InsertOrUpdateCallLinkFromSyncResult } from './server/callLinks.node.js';
|
||||||
import type { ChatFolderId, ChatFolder } from '../types/ChatFolder.std.js';
|
import type { ChatFolderId, ChatFolder } from '../types/ChatFolder.std.js';
|
||||||
import type { CurrentChatFolder } from '../types/CurrentChatFolders.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 ReadableDB = Database & { __readable_db: never };
|
||||||
export type WritableDB = ReadableDB & { __writable_db: never };
|
export type WritableDB = ReadableDB & { __writable_db: never };
|
||||||
@@ -965,6 +970,11 @@ type ReadableInterface = {
|
|||||||
hasAllChatsChatFolder: () => boolean;
|
hasAllChatsChatFolder: () => boolean;
|
||||||
getOldestDeletedChatFolder: () => ChatFolder | null;
|
getOldestDeletedChatFolder: () => ChatFolder | null;
|
||||||
|
|
||||||
|
getPinnedMessagesForConversation: (
|
||||||
|
conversationId: string
|
||||||
|
) => ReadonlyArray<PinnedMessage>;
|
||||||
|
getNextExpiringPinnedMessageAcrossConversations: () => PinnedMessage | null;
|
||||||
|
|
||||||
getMessagesNeedingUpgrade: (
|
getMessagesNeedingUpgrade: (
|
||||||
limit: number,
|
limit: number,
|
||||||
options: { maxVersion: number }
|
options: { maxVersion: number }
|
||||||
@@ -1324,6 +1334,14 @@ type WritableInterface = {
|
|||||||
messageQueueTime: number
|
messageQueueTime: number
|
||||||
) => ReadonlyArray<ChatFolderId>;
|
) => ReadonlyArray<ChatFolderId>;
|
||||||
|
|
||||||
|
createPinnedMessage: (
|
||||||
|
pinnedMessageParams: PinnedMessageParams
|
||||||
|
) => PinnedMessage;
|
||||||
|
deletePinnedMessage: (pinnedMessageId: PinnedMessageId) => void;
|
||||||
|
deleteAllExpiredPinnedMessagesBefore: (
|
||||||
|
beforeTimestamp: number
|
||||||
|
) => ReadonlyArray<PinnedMessageId>;
|
||||||
|
|
||||||
removeAll: () => void;
|
removeAll: () => void;
|
||||||
removeAllConfiguration: () => void;
|
removeAllConfiguration: () => void;
|
||||||
eraseStorageServiceState: () => void;
|
eraseStorageServiceState: () => void;
|
||||||
|
|||||||
@@ -257,6 +257,13 @@ import {
|
|||||||
updateChatFolderDeletedAtTimestampMsFromSync,
|
updateChatFolderDeletedAtTimestampMsFromSync,
|
||||||
deleteExpiredChatFolders,
|
deleteExpiredChatFolders,
|
||||||
} from './server/chatFolders.std.js';
|
} 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 { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer.std.js';
|
||||||
import type { GifType } from '../components/fun/panels/FunPanelGifs.dom.js';
|
import type { GifType } from '../components/fun/panels/FunPanelGifs.dom.js';
|
||||||
import type { NotificationProfileType } from '../types/NotificationProfile.std.js';
|
import type { NotificationProfileType } from '../types/NotificationProfile.std.js';
|
||||||
@@ -471,6 +478,9 @@ export const DataReader: ServerReadableInterface = {
|
|||||||
hasAllChatsChatFolder,
|
hasAllChatsChatFolder,
|
||||||
getOldestDeletedChatFolder,
|
getOldestDeletedChatFolder,
|
||||||
|
|
||||||
|
getPinnedMessagesForConversation,
|
||||||
|
getNextExpiringPinnedMessageAcrossConversations,
|
||||||
|
|
||||||
callLinkExists,
|
callLinkExists,
|
||||||
defunctCallLinkExists,
|
defunctCallLinkExists,
|
||||||
getAllCallLinks,
|
getAllCallLinks,
|
||||||
@@ -725,6 +735,10 @@ export const DataWriter: ServerWritableInterface = {
|
|||||||
markChatFolderDeleted,
|
markChatFolderDeleted,
|
||||||
deleteExpiredChatFolders,
|
deleteExpiredChatFolders,
|
||||||
|
|
||||||
|
createPinnedMessage,
|
||||||
|
deletePinnedMessage,
|
||||||
|
deleteAllExpiredPinnedMessagesBefore,
|
||||||
|
|
||||||
removeAll,
|
removeAll,
|
||||||
removeAllConfiguration,
|
removeAllConfiguration,
|
||||||
eraseStorageServiceState,
|
eraseStorageServiceState,
|
||||||
@@ -8136,6 +8150,7 @@ function removeAll(db: WritableDB): void {
|
|||||||
DELETE FROM messages_fts;
|
DELETE FROM messages_fts;
|
||||||
DELETE FROM messages;
|
DELETE FROM messages;
|
||||||
DELETE FROM notificationProfiles;
|
DELETE FROM notificationProfiles;
|
||||||
|
DELETE FROM pinnedMessages;
|
||||||
DELETE FROM preKeys;
|
DELETE FROM preKeys;
|
||||||
DELETE FROM reactions;
|
DELETE FROM reactions;
|
||||||
DELETE FROM recentGifs;
|
DELETE FROM recentGifs;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import type { WritableDB } from '../Interface.std.js';
|
import type { WritableDB } from '../Interface.std.js';
|
||||||
|
|
||||||
export default function updateToSchemaVersion1520(db: WritableDB): void {
|
export default function updateToSchemaVersion1550(db: WritableDB): void {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
ALTER TABLE messages
|
ALTER TABLE messages
|
||||||
ADD COLUMN hasPreviews INTEGER NOT NULL
|
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 updateToSchemaVersion1530 from './1530-update-expiring-index.std.js';
|
||||||
import updateToSchemaVersion1540 from './1540-partial-expiring-index.std.js';
|
import updateToSchemaVersion1540 from './1540-partial-expiring-index.std.js';
|
||||||
import updateToSchemaVersion1550 from './1550-has-link-preview.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';
|
import { DataWriter } from '../Server.node.js';
|
||||||
|
|
||||||
@@ -1620,6 +1621,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
|||||||
{ version: 1530, update: updateToSchemaVersion1530 },
|
{ version: 1530, update: updateToSchemaVersion1530 },
|
||||||
{ version: 1540, update: updateToSchemaVersion1540 },
|
{ version: 1540, update: updateToSchemaVersion1540 },
|
||||||
{ version: 1550, update: updateToSchemaVersion1550 },
|
{ version: 1550, update: updateToSchemaVersion1550 },
|
||||||
|
{ version: 1560, update: updateToSchemaVersion1560 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export class DBVersionFromFutureError extends Error {
|
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