mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-15 07:28:59 +00:00
Sync and update UI when pinned messages expire
This commit is contained in:
@@ -8,12 +8,14 @@ import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.js';
|
||||
import { DurationInSeconds } from '../../../util/durations/duration-in-seconds.std.js';
|
||||
import { strictAssert } from '../../../util/assert.std.js';
|
||||
import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js';
|
||||
import { isInternalFeaturesEnabled } from '../../../util/isInternalFeaturesEnabled.dom.js';
|
||||
|
||||
enum DurationOption {
|
||||
TIME_24_HOURS = 'TIME_24_HOURS',
|
||||
TIME_7_DAYS = 'TIME_7_DAYS',
|
||||
TIME_30_DAYS = 'TIME_30_DAYS',
|
||||
FOREVER = 'FOREVER',
|
||||
DEBUG_10_SECONDS = 'DEBUG_10_SECONDS',
|
||||
}
|
||||
|
||||
const DURATION_OPTIONS: Record<DurationOption, DurationInSeconds | null> = {
|
||||
@@ -21,6 +23,7 @@ const DURATION_OPTIONS: Record<DurationOption, DurationInSeconds | null> = {
|
||||
[DurationOption.TIME_7_DAYS]: DurationInSeconds.fromDays(7),
|
||||
[DurationOption.TIME_30_DAYS]: DurationInSeconds.fromDays(30),
|
||||
[DurationOption.FOREVER]: null,
|
||||
[DurationOption.DEBUG_10_SECONDS]: DurationInSeconds.fromSeconds(10),
|
||||
};
|
||||
|
||||
function isValidDurationOption(value: string): value is DurationOption {
|
||||
@@ -152,6 +155,14 @@ export const PinMessageDialog = memo(function PinMessageDialog(
|
||||
{i18n('icu:PinMessageDialog__Option--FOREVER')}
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
{isInternalFeaturesEnabled() && (
|
||||
<AxoRadioGroup.Item value={DurationOption.DEBUG_10_SECONDS}>
|
||||
<AxoRadioGroup.Indicator />
|
||||
<AxoRadioGroup.Label>
|
||||
10 seconds (Internal)
|
||||
</AxoRadioGroup.Label>
|
||||
</AxoRadioGroup.Item>
|
||||
)}
|
||||
</AxoRadioGroup.Root>
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
|
||||
@@ -292,6 +292,7 @@ const unpinMessageJobDataSchema = z.object({
|
||||
targetAuthorAci: aciSchema,
|
||||
targetSentTimestamp: z.number(),
|
||||
unpinnedAt: z.number(),
|
||||
isSyncOnly: z.boolean(),
|
||||
});
|
||||
export type UnpinMessageJobData = z.infer<typeof unpinMessageJobDataSchema>;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||
export type SendMessageJobOptions<Data> = Readonly<{
|
||||
sendName: string; // ex: 'sendExampleMessage'
|
||||
sendType: SendTypesType;
|
||||
isSyncOnly: (data: Data) => boolean;
|
||||
getMessageId: (data: Data) => string | null;
|
||||
getMessageOptions: (
|
||||
data: Data
|
||||
@@ -43,6 +44,7 @@ export function createSendMessageJob<Data>(
|
||||
const {
|
||||
sendName,
|
||||
sendType,
|
||||
isSyncOnly,
|
||||
getMessageId,
|
||||
getMessageOptions,
|
||||
getExpirationStartTimestamp,
|
||||
@@ -60,7 +62,9 @@ export function createSendMessageJob<Data>(
|
||||
getSendRecipientLists({
|
||||
log,
|
||||
conversation,
|
||||
conversationIds: Array.from(conversation.getMemberConversationIds()),
|
||||
conversationIds: isSyncOnly(data)
|
||||
? [window.ConversationController.getOurConversationIdOrThrow()]
|
||||
: Array.from(conversation.getMemberConversationIds()),
|
||||
});
|
||||
|
||||
if (untrustedServiceIds.length > 0) {
|
||||
|
||||
@@ -6,6 +6,9 @@ import { createSendMessageJob } from './createSendMessageJob.preload.js';
|
||||
export const sendPinMessage = createSendMessageJob<PinMessageJobData>({
|
||||
sendName: 'sendPinMessage',
|
||||
sendType: 'pinMessage',
|
||||
isSyncOnly() {
|
||||
return false;
|
||||
},
|
||||
getMessageId(data) {
|
||||
return data.targetMessageId;
|
||||
},
|
||||
|
||||
@@ -7,6 +7,9 @@ import { createSendMessageJob } from './createSendMessageJob.preload.js';
|
||||
export const sendUnpinMessage = createSendMessageJob<UnpinMessageJobData>({
|
||||
sendName: 'sendUnpinMessage',
|
||||
sendType: 'unpinMessage',
|
||||
isSyncOnly(data) {
|
||||
return data.isSyncOnly;
|
||||
},
|
||||
getMessageId(data) {
|
||||
return data.targetMessageId;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
|
||||
import { createExpiringEntityCleanupService } from './createExpiringEntityCleanupService.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import {
|
||||
conversationJobQueue,
|
||||
conversationQueueJobEnum,
|
||||
} from '../../jobs/conversationJobQueue.preload.js';
|
||||
import { getPinnedMessageTarget } from '../../util/getPinMessageTarget.preload.js';
|
||||
import { drop } from '../../util/drop.std.js';
|
||||
|
||||
export const pinnedMessagesCleanupService = createExpiringEntityCleanupService({
|
||||
logPrefix: 'PinnedMessages',
|
||||
@@ -23,11 +29,39 @@ export const pinnedMessagesCleanupService = createExpiringEntityCleanupService({
|
||||
};
|
||||
},
|
||||
cleanupExpiredEntities: async () => {
|
||||
const deletedPinnedMessagesIds =
|
||||
const deletedPinnedMessages =
|
||||
await DataWriter.deleteAllExpiredPinnedMessagesBefore(Date.now());
|
||||
const unpinnedAt = Date.now();
|
||||
const deletedPinnedMessagesIds = [];
|
||||
const changedConversationIds = new Set<string>();
|
||||
|
||||
for (const pinnedMessage of deletedPinnedMessages) {
|
||||
deletedPinnedMessagesIds.push(pinnedMessage.id);
|
||||
changedConversationIds.add(pinnedMessage.conversationId);
|
||||
// Add to conversation queue without waiting
|
||||
drop(sendUnpinSync(pinnedMessage.messageId, unpinnedAt));
|
||||
}
|
||||
|
||||
for (const conversationId of changedConversationIds) {
|
||||
window.reduxActions.conversations.onPinnedMessagesChanged(conversationId);
|
||||
}
|
||||
|
||||
return deletedPinnedMessagesIds;
|
||||
},
|
||||
subscribeToTriggers: () => {
|
||||
return () => null;
|
||||
},
|
||||
});
|
||||
|
||||
async function sendUnpinSync(targetMessageId: string, unpinnedAt: number) {
|
||||
const target = await getPinnedMessageTarget(targetMessageId);
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
await conversationJobQueue.add({
|
||||
type: conversationQueueJobEnum.enum.UnpinMessage,
|
||||
...target,
|
||||
unpinnedAt,
|
||||
isSyncOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1399,7 +1399,7 @@ type WritableInterface = {
|
||||
deletePinnedMessageByMessageId: (messageId: string) => PinnedMessageId | null;
|
||||
deleteAllExpiredPinnedMessagesBefore: (
|
||||
beforeTimestamp: number
|
||||
) => ReadonlyArray<PinnedMessageId>;
|
||||
) => ReadonlyArray<PinnedMessage>;
|
||||
|
||||
removeAll: () => void;
|
||||
removeAllConfiguration: () => void;
|
||||
|
||||
@@ -232,11 +232,11 @@ export function getNextExpiringPinnedMessageAcrossConversations(
|
||||
export function deleteAllExpiredPinnedMessagesBefore(
|
||||
db: WritableDB,
|
||||
beforeTimestamp: number
|
||||
): ReadonlyArray<PinnedMessageId> {
|
||||
): ReadonlyArray<PinnedMessage> {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM pinnedMessages
|
||||
WHERE expiresAt <= ${beforeTimestamp}
|
||||
RETURNING id
|
||||
RETURNING *
|
||||
`;
|
||||
return db.prepare(query, { pluck: true }).all<PinnedMessageId>(params);
|
||||
return db.prepare(query).all<PinnedMessage>(params);
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ import type { StateThunk } from '../types.std.js';
|
||||
import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js';
|
||||
import { getPinnedMessageExpiresAt } from '../../util/pinnedMessages.std.js';
|
||||
import { pinnedMessagesCleanupService } from '../../services/expiring/pinnedMessagesCleanupService.preload.js';
|
||||
import { getPinnedMessageTarget } from '../../util/getPinMessageTarget.preload.js';
|
||||
|
||||
const {
|
||||
chunk,
|
||||
@@ -5126,41 +5127,6 @@ function startAvatarDownload(
|
||||
};
|
||||
}
|
||||
|
||||
function getMessageAuthorAci(
|
||||
message: ReadonlyMessageAttributesType
|
||||
): AciString {
|
||||
if (isIncoming(message)) {
|
||||
strictAssert(
|
||||
isAciString(message.sourceServiceId),
|
||||
'Message sourceServiceId must be an ACI'
|
||||
);
|
||||
return message.sourceServiceId;
|
||||
}
|
||||
return itemStorage.user.getCheckedAci();
|
||||
}
|
||||
|
||||
type PinnedMessageTarget = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
targetMessageId: string;
|
||||
targetAuthorAci: AciString;
|
||||
targetSentTimestamp: number;
|
||||
}>;
|
||||
|
||||
async function getPinnedMessageTarget(
|
||||
targetMessageId: string
|
||||
): Promise<PinnedMessageTarget> {
|
||||
const message = await DataReader.getMessageById(targetMessageId);
|
||||
if (message == null) {
|
||||
throw new Error('getPinnedMessageTarget: Target message not found');
|
||||
}
|
||||
return {
|
||||
conversationId: message.conversationId,
|
||||
targetMessageId: message.id,
|
||||
targetAuthorAci: getMessageAuthorAci(message),
|
||||
targetSentTimestamp: message.sent_at,
|
||||
};
|
||||
}
|
||||
|
||||
function onPinnedMessagesChanged(
|
||||
conversationId: string
|
||||
): StateThunk<PinnedMessagesReplace> {
|
||||
@@ -5194,6 +5160,10 @@ function onPinnedMessageAdd(
|
||||
): StateThunk {
|
||||
return async dispatch => {
|
||||
const target = await getPinnedMessageTarget(targetMessageId);
|
||||
if (target == null) {
|
||||
throw new Error('onPinnedMessageAdd: Missing target message');
|
||||
}
|
||||
|
||||
const targetConversation = window.ConversationController.get(
|
||||
target.conversationId
|
||||
);
|
||||
@@ -5239,10 +5209,14 @@ function onPinnedMessageAdd(
|
||||
function onPinnedMessageRemove(targetMessageId: string): StateThunk {
|
||||
return async dispatch => {
|
||||
const target = await getPinnedMessageTarget(targetMessageId);
|
||||
if (target == null) {
|
||||
throw new Error('onPinnedMessageRemove: Missing target message');
|
||||
}
|
||||
await conversationJobQueue.add({
|
||||
type: conversationQueueJobEnum.enum.UnpinMessage,
|
||||
...target,
|
||||
unpinnedAt: Date.now(),
|
||||
isSyncOnly: false,
|
||||
});
|
||||
await DataWriter.deletePinnedMessageByMessageId(targetMessageId);
|
||||
drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove'));
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('sql/server/pinnedMessages', () => {
|
||||
const row3 = insertPin(getParams('c1', 'c1-m3', 3, 3)); // not expired yet
|
||||
const result = deleteAllExpiredPinnedMessagesBefore(db, 2);
|
||||
assertRows([row3]);
|
||||
assert.deepEqual(result, [row1.id, row2.id]);
|
||||
assert.deepEqual(result, [row1, row2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
45
ts/util/getPinMessageTarget.preload.ts
Normal file
45
ts/util/getPinMessageTarget.preload.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isIncoming } from '../messages/helpers.std.js';
|
||||
import type { ReadonlyMessageAttributesType } from '../model-types.js';
|
||||
import { DataReader } from '../sql/Client.preload.js';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.js';
|
||||
import type { AciString } from '../types/ServiceId.std.js';
|
||||
import { strictAssert } from './assert.std.js';
|
||||
import { isAciString } from './isAciString.std.js';
|
||||
|
||||
export type PinnedMessageTarget = Readonly<{
|
||||
conversationId: string;
|
||||
targetMessageId: string;
|
||||
targetAuthorAci: AciString;
|
||||
targetSentTimestamp: number;
|
||||
}>;
|
||||
|
||||
function getMessageAuthorAci(
|
||||
message: ReadonlyMessageAttributesType
|
||||
): AciString {
|
||||
if (isIncoming(message)) {
|
||||
strictAssert(
|
||||
isAciString(message.sourceServiceId),
|
||||
'Message sourceServiceId must be an ACI'
|
||||
);
|
||||
return message.sourceServiceId;
|
||||
}
|
||||
return itemStorage.user.getCheckedAci();
|
||||
}
|
||||
|
||||
export async function getPinnedMessageTarget(
|
||||
targetMessageId: string
|
||||
): Promise<PinnedMessageTarget | null> {
|
||||
const message = await DataReader.getMessageById(targetMessageId);
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: message.conversationId,
|
||||
targetMessageId: message.id,
|
||||
targetAuthorAci: getMessageAuthorAci(message),
|
||||
targetSentTimestamp: message.sent_at,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user