Update CallLogEvent to latest spec

This commit is contained in:
Jamie Kyle 2024-06-25 17:58:38 -07:00 committed by GitHub
parent 815fd77849
commit fc08e70c0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 366 additions and 142 deletions

View file

@ -604,7 +604,12 @@ message SyncMessage {
DELETE = 3; DELETE = 3;
} }
/* Data identifying a conversation. The service ID for 1:1, the group ID for
* group, or the room ID for an ad-hoc call. See also
* `CallLogEvent/peerId`. */
optional bytes peerId = 1; optional bytes peerId = 1;
/* An identifier for a call. Generated directly for 1:1, or derived from
* the era ID for group and ad-hoc calls. See also `CallLogEvent/callId`. */
optional uint64 callId = 2; optional uint64 callId = 2;
optional uint64 timestamp = 3; optional uint64 timestamp = 3;
optional Type type = 4; optional Type type = 4;
@ -627,10 +632,18 @@ message SyncMessage {
enum Type { enum Type {
CLEAR = 0; CLEAR = 0;
MARKED_AS_READ = 1; MARKED_AS_READ = 1;
MARKED_AS_READ_IN_CONVERSATION = 2;
} }
optional Type type = 1; optional Type type = 1;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
/* Data identifying a conversation. The service ID for 1:1, the group ID for
* group, or the room ID for an ad-hoc call. See also
* `CallEvent/peerId`. */
optional bytes peerId = 3;
/* An identifier for a call. Generated directly for 1:1, or derived from
* the era ID for group and ad-hoc calls. See also `CallEvent/callId`. */
optional uint64 callId = 4;
} }
message DeleteForMe { message DeleteForMe {
@ -641,7 +654,7 @@ message SyncMessage {
string threadE164 = 3; string threadE164 = 3;
} }
} }
message AddressableMessage { message AddressableMessage {
oneof author { oneof author {
string authorServiceId = 1; string authorServiceId = 1;
@ -682,7 +695,7 @@ message SyncMessage {
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3; repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
repeated AttachmentDelete attachmentDeletes = 4; repeated AttachmentDelete attachmentDeletes = 4;
} }
optional Sent sent = 1; optional Sent sent = 1;
optional Contacts contacts = 2; optional Contacts contacts = 2;
reserved /* groups */ 3; reserved /* groups */ 3;

View file

@ -28,6 +28,7 @@ import type {
CallHistoryFilter, CallHistoryFilter,
CallHistoryGroup, CallHistoryGroup,
CallHistoryPagination, CallHistoryPagination,
CallLogEventTarget,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import type { CallLinkStateType, CallLinkType } from '../types/CallLink'; import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload'; import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
@ -666,14 +667,17 @@ export type DataInterface = {
conversationId: string; conversationId: string;
}): Promise<MessageType | undefined>; }): Promise<MessageType | undefined>;
getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>; getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
clearCallHistory: (
target: CallLogEventTarget
) => Promise<ReadonlyArray<string>>;
markCallHistoryDeleted: (callId: string) => Promise<void>; markCallHistoryDeleted: (callId: string) => Promise<void>;
clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
cleanupCallHistoryMessages: () => Promise<void>; cleanupCallHistoryMessages: () => Promise<void>;
getCallHistoryUnreadCount(): Promise<number>; getCallHistoryUnreadCount(): Promise<number>;
markCallHistoryRead(callId: string): Promise<void>; markCallHistoryRead(callId: string): Promise<void>;
markAllCallHistoryRead( markAllCallHistoryRead(target: CallLogEventTarget): Promise<void>;
beforeTimestamp: number markAllCallHistoryReadInConversation(
): Promise<ReadonlyArray<string>>; target: CallLogEventTarget
): Promise<void>;
getCallHistoryMessageByCallId(options: { getCallHistoryMessageByCallId(options: {
conversationId: string; conversationId: string;
callId: string; callId: string;

View file

@ -157,6 +157,7 @@ import type {
CallHistoryFilter, CallHistoryFilter,
CallHistoryGroup, CallHistoryGroup,
CallHistoryPagination, CallHistoryPagination,
CallLogEventTarget,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { import {
DirectCallStatus, DirectCallStatus,
@ -166,6 +167,7 @@ import {
CallDirection, CallDirection,
GroupCallStatus, GroupCallStatus,
CallType, CallType,
CallStatusValue,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { import {
callLinkExists, callLinkExists,
@ -352,6 +354,7 @@ const dataInterface: ServerInterface = {
getCallHistoryUnreadCount, getCallHistoryUnreadCount,
markCallHistoryRead, markCallHistoryRead,
markAllCallHistoryRead, markAllCallHistoryRead,
markAllCallHistoryReadInConversation,
getCallHistoryMessageByCallId, getCallHistoryMessageByCallId,
getCallHistory, getCallHistory,
getCallHistoryGroupsCount, getCallHistoryGroupsCount,
@ -3631,34 +3634,56 @@ async function getAllCallHistory(): Promise<ReadonlyArray<CallHistoryDetails>> {
} }
async function clearCallHistory( async function clearCallHistory(
beforeTimestamp: number target: CallLogEventTarget
): Promise<Array<string>> { ): Promise<ReadonlyArray<string>> {
const db = await getWritableInstance(); const db = await getWritableInstance();
return db.transaction(() => { return db.transaction(() => {
const whereMessages = sqlFragment` const timestamp = getTimestampForCallLogEventTarget(db, target);
WHERE messages.type IS 'call-history'
AND messages.sent_at <= ${beforeTimestamp}; const [selectCallIdsQuery, selectCallIdsParams] = sql`
SELECT callsHistory.callId
FROM callsHistory
WHERE
-- Prior calls
(callsHistory.timestamp <= ${timestamp})
-- Unused call links
OR (
callsHistory.mode IS ${CALL_MODE_ADHOC} AND
callsHistory.status IS ${CALL_STATUS_PENDING}
);
`; `;
const [selectMessagesQuery, selectMessagesParams] = sql` const callIds = db
SELECT id FROM messages ${whereMessages} .prepare(selectCallIdsQuery)
`; .pluck()
const [clearMessagesQuery, clearMessagesParams] = sql` .all(selectCallIdsParams);
DELETE FROM messages ${whereMessages}
`; let deletedMessageIds: ReadonlyArray<string> = [];
batchMultiVarQuery(db, callIds, (ids: ReadonlyArray<string>): void => {
const [deleteMessagesQuery, deleteMessagesParams] = sql`
DELETE FROM messages
WHERE messages.type IS 'call-history'
AND messages.callId IN (${sqlJoin(ids)})
RETURNING id;
`;
const batchDeletedMessageIds = db
.prepare(deleteMessagesQuery)
.pluck()
.all(deleteMessagesParams);
deletedMessageIds = deletedMessageIds.concat(batchDeletedMessageIds);
});
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql` const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
UPDATE callsHistory UPDATE callsHistory
SET SET
status = ${DirectCallStatus.Deleted}, status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()} timestamp = ${Date.now()}
WHERE callsHistory.timestamp <= ${beforeTimestamp}; WHERE callsHistory.timestamp <= ${timestamp};
`; `;
const messageIds = db
.prepare(selectMessagesQuery)
.pluck()
.all(selectMessagesParams);
db.prepare(clearMessagesQuery).run(clearMessagesParams);
try { try {
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams); db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
} catch (error) { } catch (error) {
@ -3666,7 +3691,7 @@ async function clearCallHistory(
throw error; throw error;
} }
return messageIds; return deletedMessageIds;
})(); })();
} }
@ -3743,8 +3768,9 @@ async function getCallHistory(
const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen); const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen);
const SEEN_STATUS_SEEN = sqlConstant(SeenStatus.Seen); const SEEN_STATUS_SEEN = sqlConstant(SeenStatus.Seen);
const CALL_STATUS_MISSED = sqlConstant(DirectCallStatus.Missed); const CALL_STATUS_MISSED = sqlConstant(CallStatusValue.Missed);
const CALL_STATUS_DELETED = sqlConstant(DirectCallStatus.Deleted); const CALL_STATUS_DELETED = sqlConstant(CallStatusValue.Deleted);
const CALL_STATUS_PENDING = sqlConstant(CallStatusValue.Pending);
const CALL_STATUS_INCOMING = sqlConstant(CallDirection.Incoming); const CALL_STATUS_INCOMING = sqlConstant(CallDirection.Incoming);
const CALL_MODE_ADHOC = sqlConstant(CallMode.Adhoc); const CALL_MODE_ADHOC = sqlConstant(CallMode.Adhoc);
const FOUR_HOURS_IN_MS = sqlConstant(4 * 60 * 60 * 1000); const FOUR_HOURS_IN_MS = sqlConstant(4 * 60 * 60 * 1000);
@ -3781,44 +3807,88 @@ async function markCallHistoryRead(callId: string): Promise<void> {
db.prepare(query).run(params); db.prepare(query).run(params);
} }
async function markAllCallHistoryRead( function getTimestampForCallLogEventTarget(
beforeTimestamp: number db: Database,
): Promise<ReadonlyArray<string>> { target: CallLogEventTarget
const db = await getWritableInstance(); ): number {
let { timestamp } = target;
return db.transaction(() => {
const where = sqlFragment`
WHERE messages.type IS 'call-history'
AND messages.seenStatus IS ${SEEN_STATUS_UNSEEN}
AND messages.sent_at <= ${beforeTimestamp};
`;
if (target.peerId != null && target.callId != null) {
const [selectQuery, selectParams] = sql` const [selectQuery, selectParams] = sql`
SELECT DISTINCT conversationId SELECT callsHistory.timestamp
FROM messages FROM callsHistory
${where}; WHERE callsHistory.callId IS ${target.callId}
AND callsHistory.peerId IS ${target.peerId}
`; `;
const value = db.prepare(selectQuery).pluck().get(selectParams);
const conversationIds = db.prepare(selectQuery).pluck().all(selectParams); if (value != null) {
timestamp = value;
} else {
log.warn(
'getTimestampForCallLogEventTarget: Target call not found',
target.callId
);
}
}
return timestamp;
}
async function markAllCallHistoryReadWithPredicate(
target: CallLogEventTarget,
inConversation: boolean
) {
const db = await getWritableInstance();
db.transaction(() => {
const jsonPatch = JSON.stringify({ const jsonPatch = JSON.stringify({
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
}); });
const [updateQuery, updateParams] = sql` const timestamp = getTimestampForCallLogEventTarget(db, target);
UPDATE messages
SET const predicate = inConversation
seenStatus = ${SEEN_STATUS_SEEN}, ? sqlFragment`callsHistory.peerId IS ${target.peerId}`
json = json_patch(json, ${jsonPatch}) : sqlFragment`TRUE`;
${where};
const [selectQuery, selectParams] = sql`
SELECT callsHistory.callId
FROM callsHistory
WHERE ${predicate}
AND callsHistory.timestamp <= ${timestamp}
`; `;
db.prepare(updateQuery).run(updateParams); const callIds = db.prepare(selectQuery).pluck().all(selectParams);
return conversationIds; batchMultiVarQuery(db, callIds, ids => {
const idList = sqlJoin(ids.map(id => sqlFragment`${id}`));
const [updateQuery, updateParams] = sql`
UPDATE messages
SET
seenStatus = ${SEEN_STATUS_SEEN},
json = json_patch(json, ${jsonPatch})
WHERE callId IN (${idList});
`;
db.prepare(updateQuery).run(updateParams);
});
})(); })();
} }
async function markAllCallHistoryRead(
target: CallLogEventTarget
): Promise<void> {
await markAllCallHistoryReadWithPredicate(target, false);
}
async function markAllCallHistoryReadInConversation(
target: CallLogEventTarget
): Promise<void> {
strictAssert(target.peerId, 'peerId is required');
await markAllCallHistoryReadWithPredicate(target, true);
}
function getCallHistoryGroupDataSync( function getCallHistoryGroupDataSync(
db: Database, db: Database,
isCount: boolean, isCount: boolean,
@ -4932,7 +5002,7 @@ async function saveAttachmentBackupJob(
attempts, attempts,
data, data,
lastAttemptTimestamp, lastAttemptTimestamp,
mediaName, mediaName,
receivedAt, receivedAt,
retryAfter, retryAfter,
type type
@ -5003,7 +5073,7 @@ function removeAttachmentBackupJobSync(
): void { ): void {
const [query, params] = sql` const [query, params] = sql`
DELETE FROM attachment_backup_jobs DELETE FROM attachment_backup_jobs
WHERE WHERE
mediaName = ${job.mediaName}; mediaName = ${job.mediaName};
`; `;

View file

@ -18,6 +18,10 @@ import type { CallHistoryDetails } from '../../types/CallDisposition';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import { drop } from '../../util/drop'; import { drop } from '../../util/drop';
import {
getCallHistoryLatestCall,
getCallHistorySelector,
} from '../selectors/callHistory';
export type CallHistoryState = ReadonlyDeep<{ export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed. // This informs the app that underlying call history data has changed.
@ -103,15 +107,35 @@ function markCallHistoryRead(
}; };
} }
export function markCallHistoryReadInConversation(
callId: string
): ThunkAction<void, RootStateType, unknown, CallHistoryUpdateUnread> {
return async (dispatch, getState) => {
const callHistorySelector = getCallHistorySelector(getState());
const callHistory = callHistorySelector(callId);
if (callHistory == null) {
return;
}
try {
await markAllCallHistoryReadAndSync(callHistory, true);
} finally {
dispatch(updateCallHistoryUnreadCount());
}
};
}
function markCallsTabViewed(): ThunkAction< function markCallsTabViewed(): ThunkAction<
void, void,
RootStateType, RootStateType,
unknown, unknown,
CallHistoryUpdateUnread CallHistoryUpdateUnread
> { > {
return async dispatch => { return async (dispatch, getState) => {
await markAllCallHistoryReadAndSync(); const latestCall = getCallHistoryLatestCall(getState());
dispatch(updateCallHistoryUnreadCount()); if (latestCall != null) {
await markAllCallHistoryReadAndSync(latestCall, false);
dispatch(updateCallHistoryUnreadCount());
}
}; };
} }
@ -143,10 +167,13 @@ function clearAllCallHistory(): ThunkAction<
unknown, unknown,
CallHistoryReset | ToastActionType CallHistoryReset | ToastActionType
> { > {
return async dispatch => { return async (dispatch, getState) => {
try { try {
await clearCallHistoryDataAndSync(); const latestCall = getCallHistoryLatestCall(getState());
dispatch(showToast({ toastType: ToastType.CallHistoryCleared })); if (latestCall != null) {
await clearCallHistoryDataAndSync(latestCall);
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
}
} catch (error) { } catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error)); log.error('Error clearing call history', Errors.toLogFormat(error));
} finally { } finally {

View file

@ -195,6 +195,7 @@ import {
} from '../../util/deleteForMe'; } from '../../util/deleteForMe';
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types'; import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
import { isEnabled } from '../../RemoteConfig'; import { isEnabled } from '../../RemoteConfig';
import { markCallHistoryReadInConversation } from './callHistory';
import type { CapabilitiesType } from '../../textsecure/WebAPI'; import type { CapabilitiesType } from '../../textsecure/WebAPI';
// State // State
@ -1358,7 +1359,7 @@ function markMessageRead(
conversationId: string, conversationId: string,
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async (_dispatch, getState) => { return async (dispatch, getState) => {
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
throw new Error('markMessageRead: Conversation not found!'); throw new Error('markMessageRead: Conversation not found!');
@ -1382,6 +1383,12 @@ function markMessageRead(
newestSentAt: message.get('sent_at'), newestSentAt: message.get('sent_at'),
sendReadReceipts: true, sendReadReceipts: true,
}); });
if (message.get('type') === 'call-history') {
const callId = message.get('callId');
strictAssert(callId, 'callId not found');
dispatch(markCallHistoryReadInConversation(callId));
}
}; };
} }

View file

@ -4,7 +4,11 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { CallHistoryState } from '../ducks/callHistory'; import type { CallHistoryState } from '../ducks/callHistory';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { CallHistoryDetails } from '../../types/CallDisposition'; import {
AdhocCallStatus,
CallType,
type CallHistoryDetails,
} from '../../types/CallDisposition';
import { getOwn } from '../../util/getOwn'; import { getOwn } from '../../util/getOwn';
const getCallHistory = (state: StateType): CallHistoryState => const getCallHistory = (state: StateType): CallHistoryState =>
@ -36,3 +40,28 @@ export const getCallHistoryUnreadCount = createSelector(
return callHistory.unreadCount; return callHistory.unreadCount;
} }
); );
export const getCallHistoryLatestCall = createSelector(
getCallHistory,
callHistory => {
let latestCall = null;
for (const callId of Object.keys(callHistory.callHistoryByCallId)) {
const call = callHistory.callHistoryByCallId[callId];
// Skip unused call links
if (
call.type === CallType.Adhoc &&
call.status === AdhocCallStatus.Pending
) {
continue;
}
if (latestCall == null || call.timestamp > latestCall.timestamp) {
latestCall = call;
}
}
return latestCall;
}
);

View file

@ -152,9 +152,11 @@ import { chunk } from '../util/iterables';
import { inspectUnknownFieldTags } from '../util/inspectProtobufs'; import { inspectUnknownFieldTags } from '../util/inspectProtobufs';
import { incrementMessageCounter } from '../util/incrementMessageCounter'; import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { filterAndClean } from '../types/BodyRange'; import { filterAndClean } from '../types/BodyRange';
import { getCallEventForProto } from '../util/callDisposition'; import {
getCallEventForProto,
getCallLogEventForProto,
} from '../util/callDisposition';
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey'; import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
import { CallLogEvent } from '../types/CallDisposition';
import { CallLinkUpdateSyncType } from '../types/CallLink'; import { CallLinkUpdateSyncType } from '../types/CallLink';
import { bytesToUuid } from '../util/uuidToBytes'; import { bytesToUuid } from '../util/uuidToBytes';
@ -3614,32 +3616,10 @@ export default class MessageReceiver
const { receivedAtCounter } = envelope; const { receivedAtCounter } = envelope;
let event: CallLogEvent; const callLogEventDetails = getCallLogEventForProto(callLogEvent);
if (callLogEvent.type == null) {
throw new Error('MessageReceiver.handleCallLogEvent: type was null');
} else if (
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}`
);
}
if (callLogEvent.timestamp == null) {
throw new Error('MessageReceiver.handleCallLogEvent: timestamp was null');
}
const timestamp = callLogEvent.timestamp.toNumber();
const callLogEventSync = new CallLogEventSyncEvent( const callLogEventSync = new CallLogEventSyncEvent(
{ {
event, callLogEventDetails,
timestamp,
receivedAtCounter, receivedAtCounter,
}, },
this.removeFromCache.bind(this, envelope) this.removeFromCache.bind(this, envelope)

View file

@ -89,13 +89,16 @@ import type {
MessageToDelete, MessageToDelete,
} from './messageReceiverEvents'; } from './messageReceiverEvents';
import { getConversationFromTarget } from '../util/deleteForMe'; import { getConversationFromTarget } from '../util/deleteForMe';
import type { CallDetails } from '../types/CallDisposition'; import type { CallDetails, CallHistoryDetails } from '../types/CallDisposition';
import { import {
AdhocCallStatus, AdhocCallStatus,
DirectCallStatus, DirectCallStatus,
GroupCallStatus, GroupCallStatus,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { getProtoForCallHistory } from '../util/callDisposition'; import {
getBytesForPeerId,
getProtoForCallHistory,
} from '../util/callDisposition';
import { CallMode } from '../types/Calling'; import { CallMode } from '../types/Calling';
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types'; import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
@ -1589,11 +1592,15 @@ export default class MessageSender {
}; };
} }
static getClearCallHistoryMessage(timestamp: number): SingleProtoJobData { static getClearCallHistoryMessage(
latestCall: CallHistoryDetails
): SingleProtoJobData {
const ourAci = window.textsecure.storage.user.getCheckedAci(); const ourAci = window.textsecure.storage.user.getCheckedAci();
const callLogEvent = new Proto.SyncMessage.CallLogEvent({ const callLogEvent = new Proto.SyncMessage.CallLogEvent({
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR, type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
timestamp: Long.fromNumber(timestamp), timestamp: Long.fromNumber(latestCall.timestamp),
peerId: getBytesForPeerId(latestCall),
callId: Long.fromString(latestCall.callId),
}); });
const syncMessage = MessageSender.createSyncMessage(); const syncMessage = MessageSender.createSyncMessage();

View file

@ -18,7 +18,10 @@ import type {
ProcessedSent, ProcessedSent,
} from './Types.d'; } from './Types.d';
import type { ContactDetailsWithAvatar } from './ContactsParser'; import type { ContactDetailsWithAvatar } from './ContactsParser';
import type { CallEventDetails, CallLogEvent } from '../types/CallDisposition'; import type {
CallEventDetails,
CallLogEventDetails,
} from '../types/CallDisposition';
import type { CallLinkUpdateSyncType } from '../types/CallLink'; import type { CallLinkUpdateSyncType } from '../types/CallLink';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
@ -559,14 +562,13 @@ export class DeleteForMeSyncEvent extends ConfirmableEvent {
} }
export type CallLogEventSyncEventData = Readonly<{ export type CallLogEventSyncEventData = Readonly<{
event: CallLogEvent; callLogEventDetails: CallLogEventDetails;
timestamp: number;
receivedAtCounter: number; receivedAtCounter: number;
}>; }>;
export class CallLogEventSyncEvent extends ConfirmableEvent { export class CallLogEventSyncEvent extends ConfirmableEvent {
constructor( constructor(
public readonly callLogEvent: CallLogEventSyncEventData, public readonly data: CallLogEventSyncEventData,
confirm: ConfirmCallback confirm: ConfirmCallback
) { ) {
super('callLogEventSync', confirm); super('callLogEventSync', confirm);

View file

@ -26,6 +26,7 @@ export enum CallDirection {
export enum CallLogEvent { export enum CallLogEvent {
Clear = 'Clear', Clear = 'Clear',
MarkedAsRead = 'MarkedAsRead', MarkedAsRead = 'MarkedAsRead',
MarkedAsReadInConversation = 'MarkedAsReadInConversation',
} }
export enum LocalCallEvent { export enum LocalCallEvent {
@ -97,6 +98,19 @@ export type CallDetails = Readonly<{
timestamp: number; timestamp: number;
}>; }>;
export type CallLogEventTarget = Readonly<{
timestamp: number;
callId: string | null;
peerId: AciString | string | null;
}>;
export type CallLogEventDetails = Readonly<{
type: CallLogEvent;
timestamp: number;
peerId: AciString | string | null;
callId: string | null;
}>;
export type CallEventDetails = CallDetails & export type CallEventDetails = CallDetails &
Readonly<{ Readonly<{
event: CallEvent; event: CallEvent;
@ -221,6 +235,13 @@ export const callEventNormalizeSchema = z.object({
event: z.nativeEnum(Proto.SyncMessage.CallEvent.Event), event: z.nativeEnum(Proto.SyncMessage.CallEvent.Event),
}); });
export const callLogEventNormalizeSchema = z.object({
type: z.nativeEnum(Proto.SyncMessage.CallLogEvent.Type),
timestamp: longToNumberSchema,
peerId: peerIdInBytesSchema.optional(),
callId: longToStringSchema.optional(),
});
export function isSameCallHistoryGroup( export function isSameCallHistoryGroup(
a: CallHistoryGroup, a: CallHistoryGroup,
b: CallHistoryGroup b: CallHistoryGroup

View file

@ -44,6 +44,7 @@ import type {
CallEventDetails, CallEventDetails,
CallHistoryDetails, CallHistoryDetails,
CallHistoryGroup, CallHistoryGroup,
CallLogEventDetails,
CallStatus, CallStatus,
GroupCallMeta, GroupCallMeta,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
@ -60,6 +61,8 @@ import {
callDetailsSchema, callDetailsSchema,
AdhocCallStatus, AdhocCallStatus,
CallStatusValue, CallStatusValue,
callLogEventNormalizeSchema,
CallLogEvent,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
@ -248,6 +251,34 @@ export function getCallEventForProto(
}); });
} }
const callLogEventFromProto: Partial<
Record<Proto.SyncMessage.CallLogEvent.Type, CallLogEvent>
> = {
[Proto.SyncMessage.CallLogEvent.Type.CLEAR]: CallLogEvent.Clear,
[Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ]:
CallLogEvent.MarkedAsRead,
[Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION]:
CallLogEvent.MarkedAsReadInConversation,
};
export function getCallLogEventForProto(
callLogEventProto: Proto.SyncMessage.ICallLogEvent
): CallLogEventDetails {
const callLogEvent = callLogEventNormalizeSchema.parse(callLogEventProto);
const type = callLogEventFromProto[callLogEvent.type];
if (type == null) {
throw new TypeError(`Unknown call log event ${callLogEvent.type}`);
}
return {
type,
timestamp: callLogEvent.timestamp,
peerId: callLogEvent.peerId ?? null,
callId: callLogEvent.callId ?? null,
};
}
const directionToProto = { const directionToProto = {
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING, [CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING, [CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
@ -280,6 +311,17 @@ function shouldSyncStatus(callStatus: CallStatus) {
return statusToProto[callStatus] != null; return statusToProto[callStatus] != null;
} }
export function getBytesForPeerId(callHistory: CallHistoryDetails): Uint8Array {
let peerId =
callHistory.mode === CallMode.Adhoc
? Bytes.fromBase64(callHistory.peerId)
: uuidToBytes(callHistory.peerId);
if (peerId.length === 0) {
peerId = Bytes.fromBase64(callHistory.peerId);
}
return peerId;
}
export function getProtoForCallHistory( export function getProtoForCallHistory(
callHistory: CallHistoryDetails callHistory: CallHistoryDetails
): Proto.SyncMessage.ICallEvent | null { ): Proto.SyncMessage.ICallEvent | null {
@ -292,16 +334,8 @@ export function getProtoForCallHistory(
)}` )}`
); );
let peerId =
callHistory.mode === CallMode.Adhoc
? Bytes.fromBase64(callHistory.peerId)
: uuidToBytes(callHistory.peerId);
if (peerId.length === 0) {
peerId = Bytes.fromBase64(callHistory.peerId);
}
return new Proto.SyncMessage.CallEvent({ return new Proto.SyncMessage.CallEvent({
peerId, peerId: getBytesForPeerId(callHistory),
callId: Long.fromString(callHistory.callId), callId: Long.fromString(callHistory.callId),
type: typeToProto[callHistory.type], type: typeToProto[callHistory.type],
direction: directionToProto[callHistory.direction], direction: directionToProto[callHistory.direction],
@ -1191,50 +1225,63 @@ export async function updateCallHistoryFromLocalEvent(
await updateRemoteCallHistory(updatedCallHistory); await updateRemoteCallHistory(updatedCallHistory);
} }
export async function clearCallHistoryDataAndSync(): Promise<void> { export function updateDeletedMessages(messageIds: ReadonlyArray<string>): void {
messageIds.forEach(messageId => {
const message = window.MessageCache.__DEPRECATED$getById(messageId);
const conversation = message?.getConversation();
if (message == null || conversation == null) {
return;
}
window.reduxActions.conversations.messageDeleted(
messageId,
message.get('conversationId')
);
conversation.debouncedUpdateLastMessage();
window.MessageCache.__DEPRECATED$unregister(messageId);
});
}
export async function clearCallHistoryDataAndSync(
latestCall: CallHistoryDetails
): Promise<void> {
try { try {
const timestamp = Date.now(); log.info(
`clearCallHistory: Clearing call history before (${latestCall.callId}, ${latestCall.timestamp})`
log.info(`clearCallHistory: Clearing call history before ${timestamp}`); );
const messageIds = await window.Signal.Data.clearCallHistory(timestamp); const messageIds = await window.Signal.Data.clearCallHistory(latestCall);
updateDeletedMessages(messageIds);
messageIds.forEach(messageId => {
const message = window.MessageCache.__DEPRECATED$getById(messageId);
const conversation = message?.getConversation();
if (message == null || conversation == null) {
return;
}
window.reduxActions.conversations.messageDeleted(
messageId,
message.get('conversationId')
);
conversation.debouncedUpdateLastMessage();
window.MessageCache.__DEPRECATED$unregister(messageId);
});
log.info('clearCallHistory: Queueing sync message'); log.info('clearCallHistory: Queueing sync message');
await singleProtoJobQueue.add( await singleProtoJobQueue.add(
MessageSender.getClearCallHistoryMessage(timestamp) MessageSender.getClearCallHistoryMessage(latestCall)
); );
} catch (error) { } catch (error) {
log.error('clearCallHistory: Failed to clear call history', error); log.error('clearCallHistory: Failed to clear call history', error);
} }
} }
export async function markAllCallHistoryReadAndSync(): Promise<void> { export async function markAllCallHistoryReadAndSync(
latestCall: CallHistoryDetails,
inConversation: boolean
): Promise<void> {
try { try {
const timestamp = Date.now();
log.info( log.info(
`markAllCallHistoryReadAndSync: Marking call history read before ${timestamp}` `markAllCallHistoryReadAndSync: Marking call history read before (${latestCall.callId}, ${latestCall.timestamp})`
); );
await window.Signal.Data.markAllCallHistoryRead(timestamp); if (inConversation) {
await window.Signal.Data.markAllCallHistoryReadInConversation(latestCall);
} else {
await window.Signal.Data.markAllCallHistoryRead(latestCall);
}
const ourAci = window.textsecure.storage.user.getCheckedAci(); const ourAci = window.textsecure.storage.user.getCheckedAci();
const callLogEvent = new Proto.SyncMessage.CallLogEvent({ const callLogEvent = new Proto.SyncMessage.CallLogEvent({
type: Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ, type: inConversation
timestamp: Long.fromNumber(timestamp), ? Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION
: Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ,
timestamp: Long.fromNumber(latestCall.timestamp),
peerId: getBytesForPeerId(latestCall),
callId: Long.fromString(latestCall.callId),
}); });
const syncMessage = MessageSender.createSyncMessage(); const syncMessage = MessageSender.createSyncMessage();

View file

@ -3,39 +3,56 @@
import type { CallLogEventSyncEvent } from '../textsecure/messageReceiverEvents'; import type { CallLogEventSyncEvent } from '../textsecure/messageReceiverEvents';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { CallLogEventTarget } from '../types/CallDisposition';
import { CallLogEvent } from '../types/CallDisposition'; import { CallLogEvent } from '../types/CallDisposition';
import { missingCaseError } from './missingCaseError'; import { missingCaseError } from './missingCaseError';
import { strictAssert } from './assert';
import { updateDeletedMessages } from './callDisposition';
export async function onCallLogEventSync( export async function onCallLogEventSync(
syncEvent: CallLogEventSyncEvent syncEvent: CallLogEventSyncEvent
): Promise<void> { ): Promise<void> {
const { callLogEvent, confirm } = syncEvent; const { data, confirm } = syncEvent;
const { event, timestamp } = callLogEvent; const { type, peerId, callId, timestamp } = data.callLogEventDetails;
const target: CallLogEventTarget = {
peerId,
callId,
timestamp,
};
log.info( log.info(
`onCallLogEventSync: Processing event (Event: ${event}, Timestamp: ${timestamp})` `onCallLogEventSync: Processing event (Event: ${type}, CallId: ${callId}, Timestamp: ${timestamp})`
); );
if (event === CallLogEvent.Clear) { if (type === CallLogEvent.Clear) {
log.info(`onCallLogEventSync: Clearing call history before ${timestamp}`); log.info('onCallLogEventSync: Clearing call history');
try { try {
await window.Signal.Data.clearCallHistory(timestamp); const messageIds = await window.Signal.Data.clearCallHistory(target);
updateDeletedMessages(messageIds);
} finally { } finally {
// We want to reset the call history even if the clear fails. // We want to reset the call history even if the clear fails.
window.reduxActions.callHistory.resetCallHistory(); window.reduxActions.callHistory.resetCallHistory();
} }
confirm(); confirm();
} else if (event === CallLogEvent.MarkedAsRead) { } else if (type === CallLogEvent.MarkedAsRead) {
log.info( log.info('onCallLogEventSync: Marking call history read');
`onCallLogEventSync: Marking call history read before ${timestamp}`
);
try { try {
await window.Signal.Data.markAllCallHistoryRead(timestamp); await window.Signal.Data.markAllCallHistoryRead(target);
} finally {
window.reduxActions.callHistory.updateCallHistoryUnreadCount();
}
confirm();
} else if (type === CallLogEvent.MarkedAsReadInConversation) {
log.info('onCallLogEventSync: Marking call history read in conversation');
try {
strictAssert(peerId, 'Missing peerId');
await window.Signal.Data.markAllCallHistoryReadInConversation(target);
} finally { } finally {
window.reduxActions.callHistory.updateCallHistoryUnreadCount(); window.reduxActions.callHistory.updateCallHistoryUnreadCount();
} }
confirm(); confirm();
} else { } else {
throw missingCaseError(event); throw missingCaseError(type);
} }
} }