Schedule expiration of pinned messages

This commit is contained in:
Jamie
2025-12-16 09:51:03 -08:00
committed by GitHub
parent f82f84c660
commit 5d8aa316c2
6 changed files with 50 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import type { reportMessage, isOnline } from '../textsecure/WebAPI.preload.js';
import { drop } from '../util/drop.std.js';
import { CallLinkFinalizeDeleteManager } from './CallLinkFinalizeDeleteManager.preload.js';
import { chatFolderCleanupService } from '../services/expiring/chatFolderCleanupService.preload.js';
import { pinnedMessagesCleanupService } from '../services/expiring/pinnedMessagesCleanupService.preload.js';
import { callLinkRefreshJobQueue } from './callLinkRefreshJobQueue.preload.js';
import { conversationJobQueue } from './conversationJobQueue.preload.js';
import { deleteDownloadsJobQueue } from './deleteDownloadsJobQueue.preload.js';
@@ -52,6 +53,7 @@ export function initializeAllJobQueues({
drop(callLinkRefreshJobQueue.streamJobs());
drop(CallLinkFinalizeDeleteManager.start());
drop(chatFolderCleanupService.start('initializeAllJobQueues'));
drop(pinnedMessagesCleanupService.start('initializeAllJobQueues'));
}
export async function shutdownAllJobQueues(): Promise<void> {
@@ -67,5 +69,6 @@ export async function shutdownAllJobQueues(): Promise<void> {
reportSpamJobQueue.shutdown(),
CallLinkFinalizeDeleteManager.stop(),
chatFolderCleanupService.stop('shutdownAllJobQueues'),
pinnedMessagesCleanupService.stop('shutdownAllJobQueues'),
]);
}

View File

@@ -12,6 +12,8 @@ import { SignalService as Proto } from '../protobuf/index.std.js';
import type { ConversationModel } from '../models/conversations.preload.js';
import { getPinnedMessagesLimit } from '../util/pinnedMessages.dom.js';
import { getPinnedMessageExpiresAt } from '../util/pinnedMessages.std.js';
import { pinnedMessagesCleanupService } from '../services/expiring/pinnedMessagesCleanupService.preload.js';
import { drop } from '../util/drop.std.js';
const { AccessRequired } = Proto.AccessControl;
const { Role } = Proto.Member;
@@ -93,6 +95,8 @@ export async function onPinnedMessageAdd(
}
}
drop(pinnedMessagesCleanupService.trigger('onPinnedMessageAdd'));
if (result.change?.inserted) {
await targetConversation.addNotification('pinned-message-notification', {
pinnedMessageId: targetMessage.id,
@@ -144,6 +148,7 @@ export async function onPinnedMessageRemove(
log.info(
`Deleted pinned message ${deletedPinnedMessageId} for messageId ${targetMessageId}`
);
drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove'));
window.reduxActions.pinnedMessages.onPinnedMessagesChanged(
targetConversationId

View File

@@ -10,8 +10,10 @@ import { longTimeoutAsync } from '../../util/timeout.std.js';
const parentLog = createLogger('ExpiringEntityCleanupService');
type EntityId = string | number;
export type ExpiringEntity = Readonly<{
id: string;
id: EntityId;
expiresAtMs: number;
}>;
@@ -21,7 +23,7 @@ export type Unsubscribe = () => void;
export type ExpiringEntityCleanupServiceOptions = Readonly<{
logPrefix: string;
getNextExpiringEntity: () => Promise<ExpiringEntity | null>;
cleanupExpiredEntities: () => Promise<ReadonlyArray<string>>;
cleanupExpiredEntities: () => Promise<ReadonlyArray<EntityId>>;
subscribeToTriggers: (trigger: Trigger) => Unsubscribe;
_mockGetCurrentTime?: () => number;
_mockScheduleLongTimeout?: (ms: number, signal: AbortSignal) => Promise<void>;

View File

@@ -0,0 +1,33 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
import { createExpiringEntityCleanupService } from './createExpiringEntityCleanupService.std.js';
import { strictAssert } from '../../util/assert.std.js';
export const pinnedMessagesCleanupService = createExpiringEntityCleanupService({
logPrefix: 'PinnedMessages',
getNextExpiringEntity: async () => {
const nextExpiringPinnedMessage =
await DataReader.getNextExpiringPinnedMessageAcrossConversations();
if (nextExpiringPinnedMessage == null) {
return null;
}
strictAssert(
nextExpiringPinnedMessage.expiresAt != null,
'nextExpiringPinnedMessage.expiresAt is null'
);
return {
id: nextExpiringPinnedMessage.id,
expiresAtMs: nextExpiringPinnedMessage.expiresAt,
};
},
cleanupExpiredEntities: async () => {
const deletedPinnedMessagesIds =
await DataWriter.deleteAllExpiredPinnedMessagesBefore(Date.now());
return deletedPinnedMessagesIds;
},
subscribeToTriggers: () => {
return () => null;
},
});

View File

@@ -36,6 +36,8 @@ import {
MESSAGE_CHANGED,
TARGETED_CONVERSATION_CHANGED,
} from './conversations.preload.js';
import { pinnedMessagesCleanupService } from '../../services/expiring/pinnedMessagesCleanupService.preload.js';
import { drop } from '../../util/drop.std.js';
type PreloadData = ReadonlyDeep<{
conversationId: string;
@@ -156,6 +158,7 @@ function onPinnedMessageAdd(
expiresAt,
pinnedAt,
});
drop(pinnedMessagesCleanupService.trigger('onPinnedMessageAdd'));
await targetConversation.addNotification('pinned-message-notification', {
pinnedMessageId: targetMessageId,
@@ -174,7 +177,7 @@ function onPinnedMessageRemove(targetMessageId: string): StateThunk {
...target,
});
await DataWriter.deletePinnedMessageByMessageId(targetMessageId);
drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove'));
dispatch(onPinnedMessagesChanged(target.conversationId));
};
}

View File

@@ -109,7 +109,7 @@ describe('createExpiringEntityCleanupService', () => {
},
async cleanupExpiredEntities() {
calls.push('cleanupExpiredEntities');
const deletedIds: Array<string> = [];
const deletedIds: Array<string | number> = [];
const undeleted: Array<ExpiringEntity> = [];
for (const entity of mockExpiringEntities) {
if (entity.expiresAtMs <= clock.getCurrentTime()) {