diff --git a/protos/SignalService.proto b/protos/SignalService.proto index c3aeb0c14..3859b3ecd 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -614,6 +614,7 @@ message SyncMessage { message CallLogEvent { enum Type { CLEAR = 0; + MARKED_AS_READ = 1; } optional Type type = 1; diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 61506122b..879f04747 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -649,7 +649,9 @@ export type DataInterface = { cleanupCallHistoryMessages: () => Promise; getCallHistoryUnreadCount(): Promise; markCallHistoryRead(callId: string): Promise; - markAllCallHistoryRead(): Promise>; + markAllCallHistoryRead( + beforeTimestamp: number + ): Promise>; getCallHistoryMessageByCallId(options: { conversationId: string; callId: string; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 8d8f894f7..526cbf9ef 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -3487,13 +3487,16 @@ async function markCallHistoryRead(callId: string): Promise { db.prepare(query).run(params); } -async function markAllCallHistoryRead(): Promise> { +async function markAllCallHistoryRead( + beforeTimestamp: number +): Promise> { const db = await getWritableInstance(); return db.transaction(() => { const where = sqlFragment` WHERE messages.type IS 'call-history' AND messages.seenStatus IS ${SEEN_STATUS_UNSEEN} + AND messages.sent_at <= ${beforeTimestamp}; `; const [selectQuery, selectParams] = sql` diff --git a/ts/state/ducks/callHistory.ts b/ts/state/ducks/callHistory.ts index 3fc3fd70a..6fe28d188 100644 --- a/ts/state/ducks/callHistory.ts +++ b/ts/state/ducks/callHistory.ts @@ -5,7 +5,10 @@ import type { ReadonlyDeep } from 'type-fest'; import type { ThunkAction } from 'redux-thunk'; import { omit } from 'lodash'; import type { StateType as RootStateType } from '../reducer'; -import { clearCallHistoryDataAndSync } from '../../util/callDisposition'; +import { + clearCallHistoryDataAndSync, + markAllCallHistoryReadAndSync, +} from '../../util/callDisposition'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions'; import type { ToastActionType } from './toast'; @@ -107,19 +110,8 @@ function markCallsTabViewed(): ThunkAction< CallHistoryUpdateUnread > { return async dispatch => { - try { - const conversationIds = await window.Signal.Data.markAllCallHistoryRead(); - for (const conversationId of conversationIds) { - drop(window.ConversationController.get(conversationId)?.updateUnread()); - } - } catch (error) { - log.error( - 'markCallsTabViewed: Error marking all call history read', - Errors.toLogFormat(error) - ); - } finally { - dispatch(updateCallHistoryUnreadCount()); - } + await markAllCallHistoryReadAndSync(); + dispatch(updateCallHistoryUnreadCount()); }; } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index ee733d174..0adf50746 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -3505,6 +3505,10 @@ export default class MessageReceiver callLogEvent.type === Proto.SyncMessage.CallLogEvent.Type.CLEAR ) { event = CallLogEvent.Clear; + } else if ( + callLogEvent.type === Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ + ) { + event = CallLogEvent.MarkedAsRead; } else { throw new Error( `MessageReceiver.handleCallLogEvent: unknown type ${callLogEvent.type}` diff --git a/ts/types/CallDisposition.ts b/ts/types/CallDisposition.ts index ba7be52cd..599393225 100644 --- a/ts/types/CallDisposition.ts +++ b/ts/types/CallDisposition.ts @@ -23,6 +23,7 @@ export enum CallDirection { export enum CallLogEvent { Clear = 'Clear', + MarkedAsRead = 'MarkedAsRead', } export enum LocalCallEvent { diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index ec1fcd6ff..3c2b44fd3 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -1056,6 +1056,49 @@ export async function clearCallHistoryDataAndSync(): Promise { } } +export async function markAllCallHistoryReadAndSync(): Promise { + try { + const timestamp = Date.now(); + + log.info( + `markAllCallHistoryReadAndSync: Marking call history read before ${timestamp}` + ); + await window.Signal.Data.markAllCallHistoryRead(timestamp); + + const ourAci = window.textsecure.storage.user.getCheckedAci(); + + const callLogEvent = new Proto.SyncMessage.CallLogEvent({ + type: Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ, + timestamp: Long.fromNumber(timestamp), + }); + + const syncMessage = MessageSender.createSyncMessage(); + syncMessage.callLogEvent = callLogEvent; + + const contentMessage = new Proto.Content(); + contentMessage.syncMessage = syncMessage; + + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; + + log.info('markAllCallHistoryReadAndSync: Queueing sync message'); + await singleProtoJobQueue.add({ + contentHint: ContentHint.RESENDABLE, + serviceId: ourAci, + isSyncMessage: true, + protoBase64: Bytes.toBase64( + Proto.Content.encode(contentMessage).finish() + ), + type: 'callLogEventSync', + urgent: false, + }); + } catch (error) { + log.error( + 'markAllCallHistoryReadAndSync: Failed to mark call history read', + error + ); + } +} + export async function updateLocalGroupCallHistoryTimestamp( conversationId: string, callId: string, diff --git a/ts/util/onCallLogEventSync.ts b/ts/util/onCallLogEventSync.ts index a89d2a9b5..c15783fe7 100644 --- a/ts/util/onCallLogEventSync.ts +++ b/ts/util/onCallLogEventSync.ts @@ -25,6 +25,16 @@ export async function onCallLogEventSync( window.reduxActions.callHistory.resetCallHistory(); } confirm(); + } else if (event === CallLogEvent.MarkedAsRead) { + log.info( + `onCallLogEventSync: Marking call history read before ${timestamp}` + ); + try { + await window.Signal.Data.markAllCallHistoryRead(timestamp); + } finally { + window.reduxActions.callHistory.updateCallHistoryUnreadCount(); + } + confirm(); } else { throw missingCaseError(event); }