mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 12:19:41 +00:00
Add receive support for pin/unpin message
Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
@@ -70,6 +70,7 @@ import type {
|
||||
PinnedMessageId,
|
||||
PinnedMessageParams,
|
||||
} from '../types/PinnedMessage.std.js';
|
||||
import type { AppendPinnedMessageResult } from './server/pinnedMessages.std.js';
|
||||
|
||||
export type ReadableDB = Database & { __readable_db: never };
|
||||
export type WritableDB = ReadableDB & { __writable_db: never };
|
||||
@@ -1334,10 +1335,11 @@ type WritableInterface = {
|
||||
messageQueueTime: number
|
||||
) => ReadonlyArray<ChatFolderId>;
|
||||
|
||||
createPinnedMessage: (
|
||||
appendPinnedMessage: (
|
||||
pinnedMessagesLimit: number,
|
||||
pinnedMessageParams: PinnedMessageParams
|
||||
) => PinnedMessage;
|
||||
deletePinnedMessage: (pinnedMessageId: PinnedMessageId) => void;
|
||||
) => AppendPinnedMessageResult;
|
||||
deletePinnedMessageByMessageId: (messageId: string) => PinnedMessageId | null;
|
||||
deleteAllExpiredPinnedMessagesBefore: (
|
||||
beforeTimestamp: number
|
||||
) => ReadonlyArray<PinnedMessageId>;
|
||||
|
||||
@@ -260,8 +260,8 @@ import {
|
||||
import {
|
||||
getPinnedMessagesForConversation,
|
||||
getNextExpiringPinnedMessageAcrossConversations,
|
||||
createPinnedMessage,
|
||||
deletePinnedMessage,
|
||||
appendPinnedMessage,
|
||||
deletePinnedMessageByMessageId,
|
||||
deleteAllExpiredPinnedMessagesBefore,
|
||||
} from './server/pinnedMessages.std.js';
|
||||
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer.std.js';
|
||||
@@ -735,8 +735,8 @@ export const DataWriter: ServerWritableInterface = {
|
||||
markChatFolderDeleted,
|
||||
deleteExpiredChatFolders,
|
||||
|
||||
createPinnedMessage,
|
||||
deletePinnedMessage,
|
||||
appendPinnedMessage,
|
||||
deletePinnedMessageByMessageId,
|
||||
deleteAllExpiredPinnedMessagesBefore,
|
||||
|
||||
removeAll,
|
||||
@@ -988,6 +988,9 @@ export function setupTests(db: WritableDB): void {
|
||||
const silentLogger = {
|
||||
...consoleLogger,
|
||||
info: noop,
|
||||
child() {
|
||||
return silentLogger;
|
||||
},
|
||||
};
|
||||
logger = silentLogger;
|
||||
|
||||
@@ -3395,7 +3398,7 @@ function getUnreadByConversationAndMarkRead(
|
||||
conversationId = ${conversationId} AND
|
||||
${storyReplyFilter} AND
|
||||
type IN ('incoming', 'poll-terminate') AND
|
||||
hasExpireTimer IS 1 AND
|
||||
hasExpireTimer IS 1 AND
|
||||
received_at <= ${readMessageReceivedAt}
|
||||
`;
|
||||
|
||||
|
||||
17
ts/sql/migrations/1570-pinned-messages-updates.std.ts
Normal file
17
ts/sql/migrations/1570-pinned-messages-updates.std.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 updateToSchemaVersion1570(db: WritableDB): void {
|
||||
const [query] = sql`
|
||||
-- We only need the 'messageId' column
|
||||
ALTER TABLE pinnedMessages DROP COLUMN messageSentAt;
|
||||
ALTER TABLE pinnedMessages DROP COLUMN messageSenderAci;
|
||||
|
||||
-- We dont need to know who pinned the message
|
||||
ALTER TABLE pinnedMessages DROP COLUMN pinnedByAci;
|
||||
`;
|
||||
db.exec(query);
|
||||
}
|
||||
@@ -133,6 +133,7 @@ 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 updateToSchemaVersion1561 from './1561-cleanup-polls.std.js';
|
||||
import updateToSchemaVersion1570 from './1570-pinned-messages-updates.std.js';
|
||||
|
||||
import { DataWriter } from '../Server.node.js';
|
||||
|
||||
@@ -1625,6 +1626,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
||||
{ version: 1560, update: updateToSchemaVersion1560 },
|
||||
// 1561, 1551, and 1541 all refer to the same migration
|
||||
{ version: 1561, update: updateToSchemaVersion1561 },
|
||||
{ version: 1570, update: updateToSchemaVersion1570 },
|
||||
];
|
||||
|
||||
export class DBVersionFromFutureError extends Error {
|
||||
|
||||
@@ -15,14 +15,25 @@ export function getPinnedMessagesForConversation(
|
||||
conversationId: string
|
||||
): ReadonlyArray<PinnedMessage> {
|
||||
const [query, params] = sql`
|
||||
SELECT * FROM pins
|
||||
SELECT * FROM pinnedMessages
|
||||
WHERE conversationId = ${conversationId}
|
||||
ORDER BY pinnedAt DESC
|
||||
`;
|
||||
return db.prepare(query).all<PinnedMessage>(params);
|
||||
}
|
||||
|
||||
export function createPinnedMessage(
|
||||
function _getPinnedMessageByMessageId(
|
||||
db: ReadableDB,
|
||||
messageId: string
|
||||
): PinnedMessage | null {
|
||||
const [query, params] = sql`
|
||||
SELECT * FROM pinnedMessages
|
||||
WHERE messageId IS ${messageId}
|
||||
`;
|
||||
return db.prepare(query).get<PinnedMessage>(params) ?? null;
|
||||
}
|
||||
|
||||
function _insertPinnedMessage(
|
||||
db: WritableDB,
|
||||
pinnedMessageParams: PinnedMessageParams
|
||||
): PinnedMessage {
|
||||
@@ -30,17 +41,11 @@ export function createPinnedMessage(
|
||||
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}
|
||||
)
|
||||
@@ -52,13 +57,10 @@ export function createPinnedMessage(
|
||||
return row;
|
||||
}
|
||||
|
||||
export function deletePinnedMessage(
|
||||
db: WritableDB,
|
||||
pinnedMessageId: PinnedMessageId
|
||||
): void {
|
||||
function _deletePinnedMessageById(db: WritableDB, id: PinnedMessageId): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM pinnedMessages
|
||||
WHERE id = ${pinnedMessageId}
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
const result = db.prepare(query).run(params);
|
||||
strictAssert(
|
||||
@@ -67,11 +69,106 @@ export function deletePinnedMessage(
|
||||
);
|
||||
}
|
||||
|
||||
function _truncatePinnedMessagesByConversationId(
|
||||
db: WritableDB,
|
||||
conversationId: string,
|
||||
pinnedMessagesLimit: number
|
||||
): ReadonlyArray<PinnedMessageId> {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM pinnedMessages
|
||||
WHERE conversationId = ${conversationId}
|
||||
AND id NOT IN (
|
||||
SELECT id FROM pinnedMessages
|
||||
WHERE conversationId = ${conversationId}
|
||||
ORDER BY pinnedAt DESC
|
||||
LIMIT ${pinnedMessagesLimit}
|
||||
)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
return db.prepare(query, { pluck: true }).all<PinnedMessageId>(params);
|
||||
}
|
||||
|
||||
export type AppendPinnedMessageChange = Readonly<{
|
||||
inserted: PinnedMessage;
|
||||
replaced: PinnedMessageId | null;
|
||||
}>;
|
||||
|
||||
export type AppendPinnedMessageResult = Readonly<{
|
||||
change: AppendPinnedMessageChange | null;
|
||||
// Note: The `inserted` pin may immediately be truncated
|
||||
truncated: ReadonlyArray<PinnedMessageId>;
|
||||
}>;
|
||||
|
||||
export function appendPinnedMessage(
|
||||
db: WritableDB,
|
||||
pinnedMessagesLimit: number,
|
||||
pinnedMessageParams: PinnedMessageParams
|
||||
): AppendPinnedMessageResult {
|
||||
return db.transaction(() => {
|
||||
const existing = _getPinnedMessageByMessageId(
|
||||
db,
|
||||
pinnedMessageParams.messageId
|
||||
);
|
||||
|
||||
let shouldInsertOrReplace: boolean;
|
||||
if (existing == null) {
|
||||
// Always insert if there's no existing
|
||||
shouldInsertOrReplace = true;
|
||||
} else if (pinnedMessageParams.pinnedAt > existing.pinnedAt) {
|
||||
// Only replace if the pin is newer
|
||||
shouldInsertOrReplace = true;
|
||||
} else {
|
||||
shouldInsertOrReplace = false;
|
||||
}
|
||||
|
||||
let change: AppendPinnedMessageChange | null = null;
|
||||
if (shouldInsertOrReplace) {
|
||||
let replaced: PinnedMessageId | null = null;
|
||||
|
||||
if (existing != null) {
|
||||
_deletePinnedMessageById(db, existing.id);
|
||||
replaced = existing.id;
|
||||
}
|
||||
|
||||
const inserted = _insertPinnedMessage(db, pinnedMessageParams);
|
||||
|
||||
change = { inserted, replaced };
|
||||
}
|
||||
|
||||
const truncated = _truncatePinnedMessagesByConversationId(
|
||||
db,
|
||||
pinnedMessageParams.conversationId,
|
||||
pinnedMessagesLimit
|
||||
);
|
||||
|
||||
return { change, truncated };
|
||||
})();
|
||||
}
|
||||
|
||||
export function deletePinnedMessageByMessageId(
|
||||
db: WritableDB,
|
||||
messageId: string
|
||||
): PinnedMessageId | null {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM pinnedMessages
|
||||
WHERE messageId = ${messageId}
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = db
|
||||
.prepare(query, { pluck: true })
|
||||
.get<PinnedMessageId>(params);
|
||||
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
export function getNextExpiringPinnedMessageAcrossConversations(
|
||||
db: ReadableDB
|
||||
): PinnedMessage | null {
|
||||
const [query, params] = sql`
|
||||
SELECT * FROM pinnedMessages
|
||||
WHERE expiresAt IS NOT null
|
||||
ORDER BY expiresAt ASC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user