Update CallLogEvent to latest spec
This commit is contained in:
parent
815fd77849
commit
fc08e70c0f
12 changed files with 366 additions and 142 deletions
|
@ -604,7 +604,12 @@ message SyncMessage {
|
|||
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;
|
||||
/* 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 timestamp = 3;
|
||||
optional Type type = 4;
|
||||
|
@ -627,10 +632,18 @@ message SyncMessage {
|
|||
enum Type {
|
||||
CLEAR = 0;
|
||||
MARKED_AS_READ = 1;
|
||||
MARKED_AS_READ_IN_CONVERSATION = 2;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
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 {
|
||||
|
@ -641,7 +654,7 @@ message SyncMessage {
|
|||
string threadE164 = 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
message AddressableMessage {
|
||||
oneof author {
|
||||
string authorServiceId = 1;
|
||||
|
@ -682,7 +695,7 @@ message SyncMessage {
|
|||
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
|
||||
repeated AttachmentDelete attachmentDeletes = 4;
|
||||
}
|
||||
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
reserved /* groups */ 3;
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
CallHistoryFilter,
|
||||
CallHistoryGroup,
|
||||
CallHistoryPagination,
|
||||
CallLogEventTarget,
|
||||
} from '../types/CallDisposition';
|
||||
import type { CallLinkStateType, CallLinkType } from '../types/CallLink';
|
||||
import type { AttachmentDownloadJobType } from '../types/AttachmentDownload';
|
||||
|
@ -666,14 +667,17 @@ export type DataInterface = {
|
|||
conversationId: string;
|
||||
}): Promise<MessageType | undefined>;
|
||||
getAllCallHistory: () => Promise<ReadonlyArray<CallHistoryDetails>>;
|
||||
clearCallHistory: (
|
||||
target: CallLogEventTarget
|
||||
) => Promise<ReadonlyArray<string>>;
|
||||
markCallHistoryDeleted: (callId: string) => Promise<void>;
|
||||
clearCallHistory: (beforeTimestamp: number) => Promise<Array<string>>;
|
||||
cleanupCallHistoryMessages: () => Promise<void>;
|
||||
getCallHistoryUnreadCount(): Promise<number>;
|
||||
markCallHistoryRead(callId: string): Promise<void>;
|
||||
markAllCallHistoryRead(
|
||||
beforeTimestamp: number
|
||||
): Promise<ReadonlyArray<string>>;
|
||||
markAllCallHistoryRead(target: CallLogEventTarget): Promise<void>;
|
||||
markAllCallHistoryReadInConversation(
|
||||
target: CallLogEventTarget
|
||||
): Promise<void>;
|
||||
getCallHistoryMessageByCallId(options: {
|
||||
conversationId: string;
|
||||
callId: string;
|
||||
|
|
160
ts/sql/Server.ts
160
ts/sql/Server.ts
|
@ -157,6 +157,7 @@ import type {
|
|||
CallHistoryFilter,
|
||||
CallHistoryGroup,
|
||||
CallHistoryPagination,
|
||||
CallLogEventTarget,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
|
@ -166,6 +167,7 @@ import {
|
|||
CallDirection,
|
||||
GroupCallStatus,
|
||||
CallType,
|
||||
CallStatusValue,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
callLinkExists,
|
||||
|
@ -352,6 +354,7 @@ const dataInterface: ServerInterface = {
|
|||
getCallHistoryUnreadCount,
|
||||
markCallHistoryRead,
|
||||
markAllCallHistoryRead,
|
||||
markAllCallHistoryReadInConversation,
|
||||
getCallHistoryMessageByCallId,
|
||||
getCallHistory,
|
||||
getCallHistoryGroupsCount,
|
||||
|
@ -3631,34 +3634,56 @@ async function getAllCallHistory(): Promise<ReadonlyArray<CallHistoryDetails>> {
|
|||
}
|
||||
|
||||
async function clearCallHistory(
|
||||
beforeTimestamp: number
|
||||
): Promise<Array<string>> {
|
||||
target: CallLogEventTarget
|
||||
): Promise<ReadonlyArray<string>> {
|
||||
const db = await getWritableInstance();
|
||||
return db.transaction(() => {
|
||||
const whereMessages = sqlFragment`
|
||||
WHERE messages.type IS 'call-history'
|
||||
AND messages.sent_at <= ${beforeTimestamp};
|
||||
const timestamp = getTimestampForCallLogEventTarget(db, target);
|
||||
|
||||
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`
|
||||
SELECT id FROM messages ${whereMessages}
|
||||
`;
|
||||
const [clearMessagesQuery, clearMessagesParams] = sql`
|
||||
DELETE FROM messages ${whereMessages}
|
||||
`;
|
||||
const callIds = db
|
||||
.prepare(selectCallIdsQuery)
|
||||
.pluck()
|
||||
.all(selectCallIdsParams);
|
||||
|
||||
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`
|
||||
UPDATE callsHistory
|
||||
SET
|
||||
status = ${DirectCallStatus.Deleted},
|
||||
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 {
|
||||
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
|
||||
} catch (error) {
|
||||
|
@ -3666,7 +3691,7 @@ async function clearCallHistory(
|
|||
throw error;
|
||||
}
|
||||
|
||||
return messageIds;
|
||||
return deletedMessageIds;
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -3743,8 +3768,9 @@ async function getCallHistory(
|
|||
|
||||
const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen);
|
||||
const SEEN_STATUS_SEEN = sqlConstant(SeenStatus.Seen);
|
||||
const CALL_STATUS_MISSED = sqlConstant(DirectCallStatus.Missed);
|
||||
const CALL_STATUS_DELETED = sqlConstant(DirectCallStatus.Deleted);
|
||||
const CALL_STATUS_MISSED = sqlConstant(CallStatusValue.Missed);
|
||||
const CALL_STATUS_DELETED = sqlConstant(CallStatusValue.Deleted);
|
||||
const CALL_STATUS_PENDING = sqlConstant(CallStatusValue.Pending);
|
||||
const CALL_STATUS_INCOMING = sqlConstant(CallDirection.Incoming);
|
||||
const CALL_MODE_ADHOC = sqlConstant(CallMode.Adhoc);
|
||||
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);
|
||||
}
|
||||
|
||||
async function markAllCallHistoryRead(
|
||||
beforeTimestamp: number
|
||||
): Promise<ReadonlyArray<string>> {
|
||||
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};
|
||||
`;
|
||||
function getTimestampForCallLogEventTarget(
|
||||
db: Database,
|
||||
target: CallLogEventTarget
|
||||
): number {
|
||||
let { timestamp } = target;
|
||||
|
||||
if (target.peerId != null && target.callId != null) {
|
||||
const [selectQuery, selectParams] = sql`
|
||||
SELECT DISTINCT conversationId
|
||||
FROM messages
|
||||
${where};
|
||||
SELECT callsHistory.timestamp
|
||||
FROM callsHistory
|
||||
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({
|
||||
seenStatus: SeenStatus.Seen,
|
||||
});
|
||||
|
||||
const [updateQuery, updateParams] = sql`
|
||||
UPDATE messages
|
||||
SET
|
||||
seenStatus = ${SEEN_STATUS_SEEN},
|
||||
json = json_patch(json, ${jsonPatch})
|
||||
${where};
|
||||
const timestamp = getTimestampForCallLogEventTarget(db, target);
|
||||
|
||||
const predicate = inConversation
|
||||
? sqlFragment`callsHistory.peerId IS ${target.peerId}`
|
||||
: sqlFragment`TRUE`;
|
||||
|
||||
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(
|
||||
db: Database,
|
||||
isCount: boolean,
|
||||
|
@ -4932,7 +5002,7 @@ async function saveAttachmentBackupJob(
|
|||
attempts,
|
||||
data,
|
||||
lastAttemptTimestamp,
|
||||
mediaName,
|
||||
mediaName,
|
||||
receivedAt,
|
||||
retryAfter,
|
||||
type
|
||||
|
@ -5003,7 +5073,7 @@ function removeAttachmentBackupJobSync(
|
|||
): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM attachment_backup_jobs
|
||||
WHERE
|
||||
WHERE
|
||||
mediaName = ${job.mediaName};
|
||||
`;
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ import type { CallHistoryDetails } from '../../types/CallDisposition';
|
|||
import * as log from '../../logging/log';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { drop } from '../../util/drop';
|
||||
import {
|
||||
getCallHistoryLatestCall,
|
||||
getCallHistorySelector,
|
||||
} from '../selectors/callHistory';
|
||||
|
||||
export type CallHistoryState = ReadonlyDeep<{
|
||||
// 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<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
CallHistoryUpdateUnread
|
||||
> {
|
||||
return async dispatch => {
|
||||
await markAllCallHistoryReadAndSync();
|
||||
dispatch(updateCallHistoryUnreadCount());
|
||||
return async (dispatch, getState) => {
|
||||
const latestCall = getCallHistoryLatestCall(getState());
|
||||
if (latestCall != null) {
|
||||
await markAllCallHistoryReadAndSync(latestCall, false);
|
||||
dispatch(updateCallHistoryUnreadCount());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -143,10 +167,13 @@ function clearAllCallHistory(): ThunkAction<
|
|||
unknown,
|
||||
CallHistoryReset | ToastActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await clearCallHistoryDataAndSync();
|
||||
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
|
||||
const latestCall = getCallHistoryLatestCall(getState());
|
||||
if (latestCall != null) {
|
||||
await clearCallHistoryDataAndSync(latestCall);
|
||||
dispatch(showToast({ toastType: ToastType.CallHistoryCleared }));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error clearing call history', Errors.toLogFormat(error));
|
||||
} finally {
|
||||
|
|
|
@ -195,6 +195,7 @@ import {
|
|||
} from '../../util/deleteForMe';
|
||||
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
|
||||
import { isEnabled } from '../../RemoteConfig';
|
||||
import { markCallHistoryReadInConversation } from './callHistory';
|
||||
import type { CapabilitiesType } from '../../textsecure/WebAPI';
|
||||
|
||||
// State
|
||||
|
@ -1358,7 +1359,7 @@ function markMessageRead(
|
|||
conversationId: string,
|
||||
messageId: string
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async (_dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error('markMessageRead: Conversation not found!');
|
||||
|
@ -1382,6 +1383,12 @@ function markMessageRead(
|
|||
newestSentAt: message.get('sent_at'),
|
||||
sendReadReceipts: true,
|
||||
});
|
||||
|
||||
if (message.get('type') === 'call-history') {
|
||||
const callId = message.get('callId');
|
||||
strictAssert(callId, 'callId not found');
|
||||
dispatch(markCallHistoryReadInConversation(callId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import type { CallHistoryState } from '../ducks/callHistory';
|
||||
import type { StateType } from '../reducer';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallType,
|
||||
type CallHistoryDetails,
|
||||
} from '../../types/CallDisposition';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
||||
const getCallHistory = (state: StateType): CallHistoryState =>
|
||||
|
@ -36,3 +40,28 @@ export const getCallHistoryUnreadCount = createSelector(
|
|||
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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -152,9 +152,11 @@ import { chunk } from '../util/iterables';
|
|||
import { inspectUnknownFieldTags } from '../util/inspectProtobufs';
|
||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||
import { filterAndClean } from '../types/BodyRange';
|
||||
import { getCallEventForProto } from '../util/callDisposition';
|
||||
import {
|
||||
getCallEventForProto,
|
||||
getCallLogEventForProto,
|
||||
} from '../util/callDisposition';
|
||||
import { checkOurPniIdentityKey } from '../util/checkOurPniIdentityKey';
|
||||
import { CallLogEvent } from '../types/CallDisposition';
|
||||
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
import { bytesToUuid } from '../util/uuidToBytes';
|
||||
|
||||
|
@ -3614,32 +3616,10 @@ export default class MessageReceiver
|
|||
|
||||
const { receivedAtCounter } = envelope;
|
||||
|
||||
let event: 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 callLogEventDetails = getCallLogEventForProto(callLogEvent);
|
||||
const callLogEventSync = new CallLogEventSyncEvent(
|
||||
{
|
||||
event,
|
||||
timestamp,
|
||||
callLogEventDetails,
|
||||
receivedAtCounter,
|
||||
},
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
|
|
|
@ -89,13 +89,16 @@ import type {
|
|||
MessageToDelete,
|
||||
} from './messageReceiverEvents';
|
||||
import { getConversationFromTarget } from '../util/deleteForMe';
|
||||
import type { CallDetails } from '../types/CallDisposition';
|
||||
import type { CallDetails, CallHistoryDetails } from '../types/CallDisposition';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../types/CallDisposition';
|
||||
import { getProtoForCallHistory } from '../util/callDisposition';
|
||||
import {
|
||||
getBytesForPeerId,
|
||||
getProtoForCallHistory,
|
||||
} from '../util/callDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
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 callLogEvent = new Proto.SyncMessage.CallLogEvent({
|
||||
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();
|
||||
|
|
|
@ -18,7 +18,10 @@ import type {
|
|||
ProcessedSent,
|
||||
} from './Types.d';
|
||||
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 { isAciString } from '../util/isAciString';
|
||||
|
||||
|
@ -559,14 +562,13 @@ export class DeleteForMeSyncEvent extends ConfirmableEvent {
|
|||
}
|
||||
|
||||
export type CallLogEventSyncEventData = Readonly<{
|
||||
event: CallLogEvent;
|
||||
timestamp: number;
|
||||
callLogEventDetails: CallLogEventDetails;
|
||||
receivedAtCounter: number;
|
||||
}>;
|
||||
|
||||
export class CallLogEventSyncEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly callLogEvent: CallLogEventSyncEventData,
|
||||
public readonly data: CallLogEventSyncEventData,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('callLogEventSync', confirm);
|
||||
|
|
|
@ -26,6 +26,7 @@ export enum CallDirection {
|
|||
export enum CallLogEvent {
|
||||
Clear = 'Clear',
|
||||
MarkedAsRead = 'MarkedAsRead',
|
||||
MarkedAsReadInConversation = 'MarkedAsReadInConversation',
|
||||
}
|
||||
|
||||
export enum LocalCallEvent {
|
||||
|
@ -97,6 +98,19 @@ export type CallDetails = Readonly<{
|
|||
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 &
|
||||
Readonly<{
|
||||
event: CallEvent;
|
||||
|
@ -221,6 +235,13 @@ export const callEventNormalizeSchema = z.object({
|
|||
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(
|
||||
a: CallHistoryGroup,
|
||||
b: CallHistoryGroup
|
||||
|
|
|
@ -44,6 +44,7 @@ import type {
|
|||
CallEventDetails,
|
||||
CallHistoryDetails,
|
||||
CallHistoryGroup,
|
||||
CallLogEventDetails,
|
||||
CallStatus,
|
||||
GroupCallMeta,
|
||||
} from '../types/CallDisposition';
|
||||
|
@ -60,6 +61,8 @@ import {
|
|||
callDetailsSchema,
|
||||
AdhocCallStatus,
|
||||
CallStatusValue,
|
||||
callLogEventNormalizeSchema,
|
||||
CallLogEvent,
|
||||
} from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/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 = {
|
||||
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
|
||||
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
|
||||
|
@ -280,6 +311,17 @@ function shouldSyncStatus(callStatus: CallStatus) {
|
|||
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(
|
||||
callHistory: CallHistoryDetails
|
||||
): 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({
|
||||
peerId,
|
||||
peerId: getBytesForPeerId(callHistory),
|
||||
callId: Long.fromString(callHistory.callId),
|
||||
type: typeToProto[callHistory.type],
|
||||
direction: directionToProto[callHistory.direction],
|
||||
|
@ -1191,50 +1225,63 @@ export async function updateCallHistoryFromLocalEvent(
|
|||
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 {
|
||||
const timestamp = Date.now();
|
||||
|
||||
log.info(`clearCallHistory: Clearing call history before ${timestamp}`);
|
||||
const messageIds = await window.Signal.Data.clearCallHistory(timestamp);
|
||||
|
||||
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: Clearing call history before (${latestCall.callId}, ${latestCall.timestamp})`
|
||||
);
|
||||
const messageIds = await window.Signal.Data.clearCallHistory(latestCall);
|
||||
updateDeletedMessages(messageIds);
|
||||
log.info('clearCallHistory: Queueing sync message');
|
||||
await singleProtoJobQueue.add(
|
||||
MessageSender.getClearCallHistoryMessage(timestamp)
|
||||
MessageSender.getClearCallHistoryMessage(latestCall)
|
||||
);
|
||||
} catch (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 {
|
||||
const timestamp = Date.now();
|
||||
|
||||
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 callLogEvent = new Proto.SyncMessage.CallLogEvent({
|
||||
type: Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ,
|
||||
timestamp: Long.fromNumber(timestamp),
|
||||
type: inConversation
|
||||
? 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();
|
||||
|
|
|
@ -3,39 +3,56 @@
|
|||
|
||||
import type { CallLogEventSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import type { CallLogEventTarget } from '../types/CallDisposition';
|
||||
import { CallLogEvent } from '../types/CallDisposition';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import { strictAssert } from './assert';
|
||||
import { updateDeletedMessages } from './callDisposition';
|
||||
|
||||
export async function onCallLogEventSync(
|
||||
syncEvent: CallLogEventSyncEvent
|
||||
): Promise<void> {
|
||||
const { callLogEvent, confirm } = syncEvent;
|
||||
const { event, timestamp } = callLogEvent;
|
||||
const { data, confirm } = syncEvent;
|
||||
const { type, peerId, callId, timestamp } = data.callLogEventDetails;
|
||||
|
||||
const target: CallLogEventTarget = {
|
||||
peerId,
|
||||
callId,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
log.info(
|
||||
`onCallLogEventSync: Processing event (Event: ${event}, Timestamp: ${timestamp})`
|
||||
`onCallLogEventSync: Processing event (Event: ${type}, CallId: ${callId}, Timestamp: ${timestamp})`
|
||||
);
|
||||
|
||||
if (event === CallLogEvent.Clear) {
|
||||
log.info(`onCallLogEventSync: Clearing call history before ${timestamp}`);
|
||||
if (type === CallLogEvent.Clear) {
|
||||
log.info('onCallLogEventSync: Clearing call history');
|
||||
try {
|
||||
await window.Signal.Data.clearCallHistory(timestamp);
|
||||
const messageIds = await window.Signal.Data.clearCallHistory(target);
|
||||
updateDeletedMessages(messageIds);
|
||||
} finally {
|
||||
// We want to reset the call history even if the clear fails.
|
||||
window.reduxActions.callHistory.resetCallHistory();
|
||||
}
|
||||
confirm();
|
||||
} else if (event === CallLogEvent.MarkedAsRead) {
|
||||
log.info(
|
||||
`onCallLogEventSync: Marking call history read before ${timestamp}`
|
||||
);
|
||||
} else if (type === CallLogEvent.MarkedAsRead) {
|
||||
log.info('onCallLogEventSync: Marking call history read');
|
||||
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 {
|
||||
window.reduxActions.callHistory.updateCallHistoryUnreadCount();
|
||||
}
|
||||
confirm();
|
||||
} else {
|
||||
throw missingCaseError(event);
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue