From 1da25e6ddd77953c8759b96005dfe5ef5f2da03f Mon Sep 17 00:00:00 2001 From: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:59:48 -0700 Subject: [PATCH] Fix call log sync when call isnt latest message --- ts/sql/Interface.std.ts | 9 ++- ts/sql/Server.node.ts | 57 +++++++------------ ts/state/ducks/callHistory.preload.ts | 1 + .../callHistory_expiration_test.node.ts | 12 ++++ ts/util/markConversationRead.preload.ts | 31 +++++----- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/ts/sql/Interface.std.ts b/ts/sql/Interface.std.ts index 25662b4f33..4cb8d208d8 100644 --- a/ts/sql/Interface.std.ts +++ b/ts/sql/Interface.std.ts @@ -1003,6 +1003,10 @@ type ReadableInterface = { conversationId: string, eraId: string ) => boolean; + getPrevUnreadCallIdInConversation: ( + conversationId: string, + receivedAt: number + ) => string | null; callLinkExists: (roomId: string) => boolean; defunctCallLinkExists: (roomId: string) => boolean; getAllCallLinks: () => ReadonlyArray; @@ -1232,11 +1236,6 @@ type WritableInterface = { readAt?: number; storyId?: string; }) => GetUnreadByConversationAndMarkReadResultType; - getUnreadCallMessagesAndMarkRead: (options: { - conversationId: string; - readMessageReceivedAt: number; - activeCallIds: Set; - }) => GetUnreadByConversationAndMarkReadResultType; getUnreadEditedMessagesAndMarkRead: (options: { conversationId: string; readMessageReceivedAt: number; diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index 12f67c6fc9..fe3a1e321f 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -493,6 +493,7 @@ export const DataReader: ServerReadableInterface = { getCallHistoryGroupsCount, getCallHistoryGroups, hasGroupCallHistoryMessage, + getPrevUnreadCallIdInConversation, hasMedia, getSortedMedia, @@ -647,7 +648,6 @@ export const DataWriter: ServerWritableInterface = { updateAllConversationColors, removeAllProfileKeyCredentials, getUnreadByConversationAndMarkRead, - getUnreadCallMessagesAndMarkRead, getUnreadEditedMessagesAndMarkRead, getUnreadReactionsAndMarkRead, getUnreadPollVotesAndMarkRead, @@ -5496,6 +5496,27 @@ function hasGroupCallHistoryMessage( return exists === 1; } +function getPrevUnreadCallIdInConversation( + db: ReadableDB, + conversationId: string, + receivedAt: number +): string | null { + const [query, params] = sql` + SELECT messages.callId FROM messages + INNER JOIN callsHistory ON callsHistory.callId = messages.callId + WHERE messages.conversationId = ${conversationId} + AND messages.type IS 'call-history' + AND messages.seenStatus IS ${SEEN_STATUS_UNSEEN} + AND callsHistory.direction IS ${CALL_STATUS_INCOMING} + AND messages.received_at <= ${receivedAt} + ORDER BY messages.received_at DESC, messages.sent_at DESC + LIMIT 1 + `; + + const callId = db.prepare(query, { pluck: true }).get(params); + return callId ?? null; +} + function hasMedia(db: ReadableDB, conversationId: string): boolean { return db.transaction(() => { let hasAttachments: boolean; @@ -9513,40 +9534,6 @@ function _getAllEditedMessages( .all({}); } -function getUnreadCallMessagesAndMarkRead( - db: WritableDB, - { - conversationId, - readMessageReceivedAt, - activeCallIds, - }: { - conversationId: string; - readMessageReceivedAt: number; - activeCallIds: Set; - } -): GetUnreadByConversationAndMarkReadResultType { - const [query, params] = sql` - UPDATE messages - SET - expirationStartTimestamp = ${readMessageReceivedAt} - WHERE type IS 'call-history' - AND messages.conversationId IS ${conversationId} - AND messages.seenStatus IS ${SEEN_STATUS_SEEN} - AND messages.received_at <= ${readMessageReceivedAt} - AND messages.hasExpireTimer IS 1 - AND messages.expirationStartTimestamp IS NULL - AND messages.callId NOT IN (${sqlJoin(Array.from(activeCallIds))}) - RETURNING * - `; - - const rows = db.prepare(query).all(params); - const messages = hydrateMessages(db, rows); - - return messages.map(message => { - return { ...message, originalReadStatus: undefined }; - }); -} - function getUnreadEditedMessagesAndMarkRead( db: WritableDB, { diff --git a/ts/state/ducks/callHistory.preload.ts b/ts/state/ducks/callHistory.preload.ts index a4d36b80cb..0cded255bd 100644 --- a/ts/state/ducks/callHistory.preload.ts +++ b/ts/state/ducks/callHistory.preload.ts @@ -292,6 +292,7 @@ export const actions = { clearAllCallHistory, updateCallHistoryUnreadCount, markCallHistoryRead, + markCallHistoryReadInConversation, markCallsTabViewed, }; diff --git a/ts/test-mock/calling/callHistory_expiration_test.node.ts b/ts/test-mock/calling/callHistory_expiration_test.node.ts index 8ba662d104..358b147609 100644 --- a/ts/test-mock/calling/callHistory_expiration_test.node.ts +++ b/ts/test-mock/calling/callHistory_expiration_test.node.ts @@ -274,6 +274,7 @@ describe('calling/callHistoryExpiration', function (this: Mocha.Suite) { describe('1:1 calls', () => { it('expire missed call: read by opening chat', async () => { + const { phone } = bootstrap; const page = await app.getWindow(); const chatListItem = $chatListItem(page, contact.device.aci); const unreadBadge = $unreadBadge(page, contact.device.aci); @@ -312,6 +313,17 @@ describe('calling/callHistoryExpiration', function (this: Mocha.Suite) { await expect($navTabUnreadBadge(page, 'Calls')).not.toBeVisible(); await expect($missedCall(page)).toBeVisible(); + debug('waiting for sync message'); + const syncMessage = await phone.waitForSyncMessage(entry => { + return ( + entry.syncMessage.content?.callLogEvent?.type === + Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION + ); + }); + expect(syncMessage.syncMessage.content?.callLogEvent?.callId).toBe( + BigInt(callTimestamp) + ); + debug('waiting for message to expire'); await delay(EXPIRE_TIMER_MS); await expect($missedCall(page)).not.toBeVisible(); diff --git a/ts/util/markConversationRead.preload.ts b/ts/util/markConversationRead.preload.ts index 6aa81529fd..3bcef6d8b1 100644 --- a/ts/util/markConversationRead.preload.ts +++ b/ts/util/markConversationRead.preload.ts @@ -4,7 +4,7 @@ import lodash from 'lodash'; import type { ConversationAttributesType } from '../model-types.d.ts'; -import { DataWriter } from '../sql/Client.preload.ts'; +import { DataReader, DataWriter } from '../sql/Client.preload.ts'; import { hasErrors } from '../state/selectors/message.preload.ts'; import { readSyncJobQueue } from '../jobs/readSyncJobQueue.preload.ts'; import { notificationService } from '../services/notifications.preload.ts'; @@ -28,7 +28,6 @@ import { isAciString } from './isAciString.std.ts'; import type { MessageModel } from '../models/messages.preload.ts'; import { postSaveUpdates } from './cleanup.preload.ts'; import { itemStorage } from '../textsecure/Storage.preload.ts'; -import { calling } from '../services/calling.preload.ts'; const { isNumber, pick } = lodash; @@ -48,10 +47,10 @@ export async function markConversationRead( const [ unreadMessages, - unreadCallMessages, unreadEditedMessages, unreadReactions, unreadPollVotes, + lastUnreadCallId, ] = await Promise.all([ DataWriter.getUnreadByConversationAndMarkRead({ conversationId, @@ -59,11 +58,6 @@ export async function markConversationRead( readAt: options.readAt, includeStoryReplies: !isGroup(conversationAttrs), }), - DataWriter.getUnreadCallMessagesAndMarkRead({ - conversationId, - readMessageReceivedAt: readMessage.received_at, - activeCallIds: calling.getActiveCallIds(), - }), DataWriter.getUnreadEditedMessagesAndMarkRead({ conversationId, readMessageReceivedAt: readMessage.received_at, @@ -76,6 +70,10 @@ export async function markConversationRead( conversationId, readMessageReceivedAt: readMessage.received_at, }), + DataReader.getPrevUnreadCallIdInConversation( + conversationId, + readMessage.received_at + ), ]); const convoId = getConversationIdForLogging(conversationAttrs); @@ -87,7 +85,6 @@ export async function markConversationRead( receivedAt: readMessage.received_at, }, unreadMessages: unreadMessages.length, - unreadCallMessages: unreadCallMessages.length, unreadEditedMessages: unreadEditedMessages.length, unreadReactions: unreadReactions.length, unreadPollVotes: unreadPollVotes.length, @@ -95,10 +92,10 @@ export async function markConversationRead( if ( !unreadMessages.length && - !unreadCallMessages.length && !unreadEditedMessages.length && !unreadReactions.length && - !unreadPollVotes.length + !unreadPollVotes.length && + lastUnreadCallId == null ) { return false; } @@ -148,11 +145,7 @@ export async function markConversationRead( }); }); - const allUnreadMessages = [ - ...unreadMessages, - ...unreadCallMessages, - ...unreadEditedMessages, - ]; + const allUnreadMessages = [...unreadMessages, ...unreadEditedMessages]; const updatedMessages: Array = []; @@ -274,6 +267,12 @@ export async function markConversationRead( } } + if (lastUnreadCallId != null) { + window.reduxActions.callHistory.markCallHistoryReadInConversation( + lastUnreadCallId + ); + } + updateExpiringMessagesService(); tapToViewMessagesDeletionService.update();