Calls Tab & Group Call Disposition
This commit is contained in:
parent
620e85ca01
commit
1eaabb6734
139 changed files with 9182 additions and 2721 deletions
907
ts/util/callDisposition.ts
Normal file
907
ts/util/callDisposition.ts
Normal file
|
@ -0,0 +1,907 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Long from 'long';
|
||||
import type {
|
||||
Call,
|
||||
GroupCall,
|
||||
PeekInfo,
|
||||
LocalDeviceState,
|
||||
} from '@signalapp/ringrtc';
|
||||
import {
|
||||
CallState,
|
||||
ConnectionState,
|
||||
JoinState,
|
||||
callIdFromEra,
|
||||
callIdFromRingId,
|
||||
RingUpdate,
|
||||
} from '@signalapp/ringrtc';
|
||||
import { v4 as generateGuid } from 'uuid';
|
||||
import { strictAssert } from './assert';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { bytesToUuid, uuidToBytes } from './uuidToBytes';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import { CallEndedReason, CallMode } from '../types/Calling';
|
||||
import { isMe } from './whatTypeOfConversation';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { incrementMessageCounter } from './incrementMessageCounter';
|
||||
import { ReadStatus, maxReadStatus } from '../messages/MessageReadStatus';
|
||||
import { SeenStatus, maxSeenStatus } from '../MessageSeenStatus';
|
||||
import { canConversationBeUnarchived } from './canConversationBeUnarchived';
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
MessageAttributesType,
|
||||
} from '../model-types';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import * as Bytes from '../Bytes';
|
||||
import type {
|
||||
CallDetails,
|
||||
CallEvent,
|
||||
CallEventDetails,
|
||||
CallHistoryDetails,
|
||||
CallHistoryGroup,
|
||||
CallStatus,
|
||||
GroupCallMeta,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
callEventNormalizeSchema,
|
||||
CallType,
|
||||
CallDirection,
|
||||
callEventDetailsSchema,
|
||||
LocalCallEvent,
|
||||
RemoteCallEvent,
|
||||
callHistoryDetailsSchema,
|
||||
callDetailsSchema,
|
||||
} from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { drop } from './drop';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
||||
export function formatCallEvent(callEvent: CallEventDetails): string {
|
||||
const { callId, peerId, direction, event, type, mode, timestamp } = callEvent;
|
||||
return `CallEvent (${callId}, ${peerId}, ${mode}, ${event}, ${direction}, ${type}, ${mode}, ${timestamp})`;
|
||||
}
|
||||
|
||||
export function formatCallHistory(callHistory: CallHistoryDetails): string {
|
||||
const { callId, peerId, direction, status, type, mode, timestamp } =
|
||||
callHistory;
|
||||
return `CallHistory (${callId}, ${peerId}, ${mode}, ${status}, ${direction}, ${type}, ${mode}, ${timestamp})`;
|
||||
}
|
||||
|
||||
export function formatCallHistoryGroup(
|
||||
callHistoryGroup: CallHistoryGroup
|
||||
): string {
|
||||
const { peerId, direction, status, type, mode, timestamp } = callHistoryGroup;
|
||||
return `CallHistoryGroup (${peerId}, ${mode}, ${status}, ${direction}, ${type}, ${mode}, ${timestamp})`;
|
||||
}
|
||||
|
||||
export function formatPeekInfo(peekInfo: PeekInfo): string {
|
||||
const { eraId, deviceCount, creator } = peekInfo;
|
||||
const callId = eraId != null ? getCallIdFromEra(eraId) : null;
|
||||
const creatorUuid = creator != null ? getCreatorUuid(creator) : null;
|
||||
return `PeekInfo (${eraId}, ${callId}, ${creatorUuid}, ${deviceCount})`;
|
||||
}
|
||||
|
||||
export function formatLocalDeviceState(
|
||||
localDeviceState: LocalDeviceState
|
||||
): string {
|
||||
const connectionState = ConnectionState[localDeviceState.connectionState];
|
||||
const joinState = JoinState[localDeviceState.joinState];
|
||||
return `LocalDeviceState (${connectionState}, ${joinState})`;
|
||||
}
|
||||
|
||||
export function getCallIdFromRing(ringId: bigint): string {
|
||||
return Long.fromValue(callIdFromRingId(ringId)).toString();
|
||||
}
|
||||
|
||||
export function getCallIdFromEra(eraId: string): string {
|
||||
return Long.fromValue(callIdFromEra(eraId)).toString();
|
||||
}
|
||||
|
||||
export function getCreatorUuid(creator: Buffer): string {
|
||||
const uuid = bytesToUuid(creator);
|
||||
strictAssert(uuid != null, 'creator uuid buffer was not a valid uuid');
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export function getGroupCallMeta(
|
||||
peekInfo: PeekInfo | null
|
||||
): GroupCallMeta | null {
|
||||
if (peekInfo?.eraId == null || peekInfo?.creator == null) {
|
||||
return null;
|
||||
}
|
||||
const callId = getCallIdFromEra(peekInfo.eraId);
|
||||
const ringerId = bytesToUuid(peekInfo.creator);
|
||||
strictAssert(ringerId != null, 'peekInfo.creator was invalid uuid');
|
||||
return { callId, ringerId };
|
||||
}
|
||||
|
||||
export function getPeerIdFromConversation(
|
||||
conversation: ConversationAttributesType | ConversationType
|
||||
): string {
|
||||
if (conversation.type === 'direct' || conversation.type === 'private') {
|
||||
strictAssert(conversation.uuid != null, 'UUID must exist for direct chat');
|
||||
return conversation.uuid;
|
||||
}
|
||||
strictAssert(
|
||||
conversation.groupId != null,
|
||||
'groupId must exist for group chat'
|
||||
);
|
||||
return conversation.groupId;
|
||||
}
|
||||
|
||||
// Call Events <-> Protos
|
||||
// ----------------------
|
||||
|
||||
export function getCallEventForProto(
|
||||
callEventProto: Proto.SyncMessage.ICallEvent
|
||||
): CallEventDetails {
|
||||
const callEvent = callEventNormalizeSchema.parse(callEventProto);
|
||||
const { callId, peerId, timestamp } = callEvent;
|
||||
|
||||
let type: CallType;
|
||||
if (callEvent.type === Proto.SyncMessage.CallEvent.Type.GROUP_CALL) {
|
||||
type = CallType.Group;
|
||||
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.AUDIO_CALL) {
|
||||
type = CallType.Audio;
|
||||
} else if (callEvent.type === Proto.SyncMessage.CallEvent.Type.VIDEO_CALL) {
|
||||
type = CallType.Video;
|
||||
} else {
|
||||
throw new TypeError(`Unknown call type ${callEvent.type}`);
|
||||
}
|
||||
|
||||
let mode: CallMode;
|
||||
if (type === CallType.Group) {
|
||||
mode = CallMode.Group;
|
||||
} else {
|
||||
mode = CallMode.Direct;
|
||||
}
|
||||
|
||||
let direction: CallDirection;
|
||||
if (callEvent.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING) {
|
||||
direction = CallDirection.Incoming;
|
||||
} else if (
|
||||
callEvent.direction === Proto.SyncMessage.CallEvent.Direction.OUTGOING
|
||||
) {
|
||||
direction = CallDirection.Outgoing;
|
||||
} else {
|
||||
throw new TypeError(`Unknown call direction ${callEvent.direction}`);
|
||||
}
|
||||
|
||||
let event: RemoteCallEvent;
|
||||
if (callEvent.event === Proto.SyncMessage.CallEvent.Event.ACCEPTED) {
|
||||
event = RemoteCallEvent.Accepted;
|
||||
} else if (
|
||||
callEvent.event === Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED
|
||||
) {
|
||||
event = RemoteCallEvent.NotAccepted;
|
||||
} else if (callEvent.event === Proto.SyncMessage.CallEvent.Event.DELETE) {
|
||||
event = RemoteCallEvent.Delete;
|
||||
} else {
|
||||
throw new TypeError(`Unknown call event ${callEvent.event}`);
|
||||
}
|
||||
|
||||
return callEventDetailsSchema.parse({
|
||||
callId,
|
||||
peerId,
|
||||
ringerId: null,
|
||||
mode,
|
||||
type,
|
||||
direction,
|
||||
timestamp,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
const directionToProto = {
|
||||
[CallDirection.Incoming]: Proto.SyncMessage.CallEvent.Direction.INCOMING,
|
||||
[CallDirection.Outgoing]: Proto.SyncMessage.CallEvent.Direction.OUTGOING,
|
||||
};
|
||||
|
||||
const typeToProto = {
|
||||
[CallType.Audio]: Proto.SyncMessage.CallEvent.Type.AUDIO_CALL,
|
||||
[CallType.Video]: Proto.SyncMessage.CallEvent.Type.VIDEO_CALL,
|
||||
[CallType.Group]: Proto.SyncMessage.CallEvent.Type.GROUP_CALL,
|
||||
};
|
||||
|
||||
const statusToProto: Record<
|
||||
CallStatus,
|
||||
Proto.SyncMessage.CallEvent.Event | null
|
||||
> = {
|
||||
[DirectCallStatus.Accepted]: Proto.SyncMessage.CallEvent.Event.ACCEPTED, // and GroupCallStatus.Accepted
|
||||
[DirectCallStatus.Declined]: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED, // and GroupCallStatus.Declined
|
||||
[DirectCallStatus.Deleted]: Proto.SyncMessage.CallEvent.Event.DELETE, // and GroupCallStatus.Deleted
|
||||
[DirectCallStatus.Missed]: null, // and GroupCallStatus.Missed
|
||||
[DirectCallStatus.Pending]: null,
|
||||
[GroupCallStatus.GenericGroupCall]: null,
|
||||
[GroupCallStatus.Joined]: null,
|
||||
[GroupCallStatus.OutgoingRing]: null,
|
||||
[GroupCallStatus.Ringing]: null,
|
||||
};
|
||||
|
||||
function shouldSyncStatus(callStatus: CallStatus) {
|
||||
return statusToProto[callStatus] != null;
|
||||
}
|
||||
|
||||
function getProtoForCallHistory(
|
||||
callHistory: CallHistoryDetails
|
||||
): Proto.SyncMessage.ICallEvent | null {
|
||||
const event = statusToProto[callHistory.status];
|
||||
|
||||
strictAssert(
|
||||
event != null,
|
||||
`getProtoForCallHistory: Cannot create proto for status ${formatCallHistory(
|
||||
callHistory
|
||||
)}`
|
||||
);
|
||||
|
||||
let peerId = uuidToBytes(callHistory.peerId);
|
||||
if (peerId.length === 0) {
|
||||
peerId = Bytes.fromBase64(callHistory.peerId);
|
||||
}
|
||||
|
||||
return new Proto.SyncMessage.CallEvent({
|
||||
peerId,
|
||||
callId: Long.fromString(callHistory.callId),
|
||||
type: typeToProto[callHistory.type],
|
||||
direction: directionToProto[callHistory.direction],
|
||||
event,
|
||||
timestamp: Long.fromNumber(callHistory.timestamp),
|
||||
});
|
||||
}
|
||||
|
||||
// Local Events
|
||||
// ------------
|
||||
|
||||
const endedReasonToEvent: Record<CallEndedReason, LocalCallEvent> = {
|
||||
// Accepted
|
||||
[CallEndedReason.AcceptedOnAnotherDevice]: LocalCallEvent.Accepted,
|
||||
// Hangup (Incoming = Declined, Outgoing = Missed)
|
||||
[CallEndedReason.RemoteHangup]: LocalCallEvent.RemoteHangup,
|
||||
[CallEndedReason.LocalHangup]: LocalCallEvent.Hangup,
|
||||
// Missed
|
||||
[CallEndedReason.Busy]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.BusyOnAnotherDevice]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.ConnectionFailure]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.Glare]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.GlareFailure]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.InternalFailure]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.ReCall]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.ReceivedOfferExpired]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.ReceivedOfferWhileActive]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.ReceivedOfferWithGlare]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.RemoteHangupNeedPermission]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.SignalingFailure]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.Timeout]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.Declined]: LocalCallEvent.Missed,
|
||||
[CallEndedReason.DeclinedOnAnotherDevice]: LocalCallEvent.Missed,
|
||||
};
|
||||
|
||||
export function getLocalCallEventFromCallEndedReason(
|
||||
callEndedReason: CallEndedReason
|
||||
): LocalCallEvent {
|
||||
log.info('getLocalCallEventFromCallEndedReason', callEndedReason);
|
||||
return endedReasonToEvent[callEndedReason];
|
||||
}
|
||||
|
||||
export function getLocalCallEventFromDirectCall(
|
||||
call: Call
|
||||
): LocalCallEvent | null {
|
||||
log.info('getLocalCallEventFromDirectCall', call.state);
|
||||
if (call.state === CallState.Accepted) {
|
||||
return LocalCallEvent.Accepted;
|
||||
}
|
||||
if (call.state === CallState.Ended) {
|
||||
strictAssert(call.endedReason != null, 'Call ended without reason');
|
||||
return getLocalCallEventFromCallEndedReason(call.endedReason);
|
||||
}
|
||||
if (call.state === CallState.Ringing) {
|
||||
return LocalCallEvent.Ringing;
|
||||
}
|
||||
if (call.state === CallState.Prering) {
|
||||
return null;
|
||||
}
|
||||
if (call.state === CallState.Reconnecting) {
|
||||
return null;
|
||||
}
|
||||
throw missingCaseError(call.state);
|
||||
}
|
||||
|
||||
const ringUpdateToEvent: Record<RingUpdate, LocalCallEvent> = {
|
||||
[RingUpdate.AcceptedOnAnotherDevice]: LocalCallEvent.Accepted,
|
||||
[RingUpdate.BusyLocally]: LocalCallEvent.Missed,
|
||||
[RingUpdate.BusyOnAnotherDevice]: LocalCallEvent.Missed,
|
||||
[RingUpdate.CancelledByRinger]: LocalCallEvent.Missed,
|
||||
[RingUpdate.DeclinedOnAnotherDevice]: LocalCallEvent.Missed,
|
||||
[RingUpdate.ExpiredRequest]: LocalCallEvent.Missed,
|
||||
[RingUpdate.Requested]: LocalCallEvent.Ringing,
|
||||
};
|
||||
|
||||
export function getLocalCallEventFromRingUpdate(
|
||||
update: RingUpdate
|
||||
): LocalCallEvent | null {
|
||||
log.info('getLocalCallEventFromRingUpdate', RingUpdate[update]);
|
||||
return ringUpdateToEvent[update];
|
||||
}
|
||||
|
||||
export function getLocalCallEventFromGroupCall(
|
||||
groupCall: GroupCall,
|
||||
groupCallMeta: GroupCallMeta
|
||||
): LocalCallEvent | null {
|
||||
const direction = getCallDirectionFromRingerId(groupCallMeta.ringerId);
|
||||
const localDeviceState = groupCall.getLocalDeviceState();
|
||||
log.info(
|
||||
'getLocalCallEventFromGroupCall',
|
||||
direction,
|
||||
JoinState[localDeviceState.joinState]
|
||||
);
|
||||
if (direction === CallDirection.Incoming) {
|
||||
if (localDeviceState.joinState === JoinState.Joined) {
|
||||
return LocalCallEvent.Accepted;
|
||||
}
|
||||
if (localDeviceState.joinState === JoinState.NotJoined) {
|
||||
return null; // Group calls shouldn't send "NotAccepted"
|
||||
}
|
||||
if (localDeviceState.joinState === JoinState.Joining) {
|
||||
return LocalCallEvent.Accepted;
|
||||
}
|
||||
throw missingCaseError(localDeviceState.joinState);
|
||||
} else {
|
||||
if (localDeviceState.joinState === JoinState.NotJoined) {
|
||||
return LocalCallEvent.Hangup;
|
||||
}
|
||||
return LocalCallEvent.Ringing;
|
||||
}
|
||||
}
|
||||
|
||||
// Call Direction
|
||||
// --------------
|
||||
|
||||
function getCallDirectionFromRingerId(ringerId: string): CallDirection {
|
||||
const ringerConversation = window.ConversationController.get(ringerId);
|
||||
strictAssert(
|
||||
ringerConversation != null,
|
||||
`getCallDirectionFromRingerId: Missing ringer conversation (${ringerId})`
|
||||
);
|
||||
const direction = isMe(ringerConversation.attributes)
|
||||
? CallDirection.Outgoing
|
||||
: CallDirection.Incoming;
|
||||
return direction;
|
||||
}
|
||||
|
||||
// Call Details
|
||||
// ------------
|
||||
|
||||
export function getCallDetailsFromDirectCall(
|
||||
peerId: string,
|
||||
call: Call
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
callId: Long.fromValue(call.callId).toString(),
|
||||
peerId,
|
||||
ringerId: call.isIncoming ? call.remoteUserId : null,
|
||||
mode: CallMode.Direct,
|
||||
type: call.isVideoCall ? CallType.Video : CallType.Audio,
|
||||
direction: call.isIncoming
|
||||
? CallDirection.Incoming
|
||||
: CallDirection.Outgoing,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export function getCallDetailsFromEndedDirectCall(
|
||||
callId: string,
|
||||
peerId: string,
|
||||
ringerId: string,
|
||||
wasVideoCall: boolean,
|
||||
timestamp: number
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
mode: CallMode.Direct,
|
||||
type: wasVideoCall ? CallType.Video : CallType.Audio,
|
||||
direction: getCallDirectionFromRingerId(ringerId),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCallDetailsFromGroupCallMeta(
|
||||
peerId: string,
|
||||
groupCallMeta: GroupCallMeta
|
||||
): CallDetails {
|
||||
return callDetailsSchema.parse({
|
||||
callId: groupCallMeta.callId,
|
||||
peerId,
|
||||
ringerId: groupCallMeta.ringerId,
|
||||
mode: CallMode.Group,
|
||||
type: CallType.Group,
|
||||
direction: getCallDirectionFromRingerId(groupCallMeta.ringerId),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Call Event Details
|
||||
// ------------------
|
||||
|
||||
export function getCallEventDetails(
|
||||
callDetails: CallDetails,
|
||||
event: LocalCallEvent
|
||||
): CallEventDetails {
|
||||
return callEventDetailsSchema.parse({ ...callDetails, event });
|
||||
}
|
||||
|
||||
// transitions
|
||||
// -----------
|
||||
|
||||
export function transitionCallHistory(
|
||||
callHistory: CallHistoryDetails | null,
|
||||
callEvent: CallEventDetails
|
||||
): CallHistoryDetails {
|
||||
const { callId, peerId, ringerId, mode, type, direction, event } = callEvent;
|
||||
|
||||
if (callHistory != null) {
|
||||
strictAssert(callHistory.callId === callId, 'callId must be same');
|
||||
strictAssert(callHistory.peerId === peerId, 'peerId must be same');
|
||||
strictAssert(
|
||||
ringerId == null || callHistory.ringerId === ringerId,
|
||||
'ringerId must be same if it exists'
|
||||
);
|
||||
strictAssert(callHistory.direction === direction, 'direction must be same');
|
||||
strictAssert(callHistory.type === type, 'type must be same');
|
||||
strictAssert(callHistory.mode === mode, 'mode must be same');
|
||||
}
|
||||
|
||||
const prevStatus = callHistory?.status ?? null;
|
||||
let status: DirectCallStatus | GroupCallStatus;
|
||||
|
||||
if (mode === CallMode.Direct) {
|
||||
status = transitionDirectCallStatus(
|
||||
prevStatus as DirectCallStatus | null,
|
||||
event,
|
||||
direction
|
||||
);
|
||||
} else if (mode === CallMode.Group) {
|
||||
status = transitionGroupCallStatus(
|
||||
prevStatus as GroupCallStatus | null,
|
||||
event,
|
||||
direction
|
||||
);
|
||||
} else if (mode === CallMode.None) {
|
||||
throw new TypeError('Call mode must not be none');
|
||||
} else {
|
||||
throw missingCaseError(mode);
|
||||
}
|
||||
|
||||
const timestamp = Math.max(callEvent.timestamp, callHistory?.timestamp ?? 0);
|
||||
|
||||
return callHistoryDetailsSchema.parse({
|
||||
callId,
|
||||
peerId,
|
||||
ringerId,
|
||||
mode,
|
||||
type,
|
||||
direction,
|
||||
timestamp,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
function transitionDirectCallStatus(
|
||||
status: DirectCallStatus | null,
|
||||
callEvent: CallEvent,
|
||||
direction: CallDirection
|
||||
): DirectCallStatus {
|
||||
log.info('transitionDirectCallStatus', status, callEvent, direction);
|
||||
// In all cases if we get a delete event, we need to delete the call, and never
|
||||
// transition from deleted.
|
||||
if (
|
||||
callEvent === RemoteCallEvent.Delete ||
|
||||
callEvent === LocalCallEvent.Delete ||
|
||||
status === DirectCallStatus.Deleted
|
||||
) {
|
||||
return DirectCallStatus.Deleted;
|
||||
}
|
||||
|
||||
if (
|
||||
callEvent === RemoteCallEvent.Accepted ||
|
||||
callEvent === LocalCallEvent.Accepted
|
||||
) {
|
||||
return DirectCallStatus.Accepted;
|
||||
}
|
||||
|
||||
if (status === DirectCallStatus.Accepted) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (callEvent === RemoteCallEvent.NotAccepted) {
|
||||
if (status === DirectCallStatus.Declined) {
|
||||
return DirectCallStatus.Declined;
|
||||
}
|
||||
return DirectCallStatus.Missed;
|
||||
}
|
||||
|
||||
if (callEvent === LocalCallEvent.Missed) {
|
||||
return DirectCallStatus.Missed;
|
||||
}
|
||||
|
||||
if (callEvent === LocalCallEvent.Declined) {
|
||||
return DirectCallStatus.Declined;
|
||||
}
|
||||
|
||||
if (callEvent === LocalCallEvent.Hangup) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return DirectCallStatus.Declined;
|
||||
}
|
||||
return DirectCallStatus.Missed;
|
||||
}
|
||||
|
||||
if (callEvent === LocalCallEvent.RemoteHangup) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return DirectCallStatus.Missed;
|
||||
}
|
||||
return DirectCallStatus.Declined;
|
||||
}
|
||||
|
||||
if (
|
||||
callEvent === LocalCallEvent.Started ||
|
||||
callEvent === LocalCallEvent.Ringing
|
||||
) {
|
||||
return DirectCallStatus.Pending;
|
||||
}
|
||||
|
||||
throw missingCaseError(callEvent);
|
||||
}
|
||||
|
||||
function transitionGroupCallStatus(
|
||||
status: GroupCallStatus | null,
|
||||
event: CallEvent,
|
||||
direction: CallDirection
|
||||
): GroupCallStatus {
|
||||
log.info('transitionGroupCallStatus', status, event, direction);
|
||||
|
||||
// In all cases if we get a delete event, we need to delete the call, and never
|
||||
// transition from deleted.
|
||||
if (
|
||||
event === RemoteCallEvent.Delete ||
|
||||
event === LocalCallEvent.Delete ||
|
||||
status === GroupCallStatus.Deleted
|
||||
) {
|
||||
return GroupCallStatus.Deleted;
|
||||
}
|
||||
|
||||
if (status === GroupCallStatus.Accepted) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (event === RemoteCallEvent.NotAccepted) {
|
||||
throw new Error(`callHistoryDetails: Group calls shouldn't send ${event}`);
|
||||
}
|
||||
|
||||
if (event === RemoteCallEvent.Accepted || event === LocalCallEvent.Accepted) {
|
||||
switch (status) {
|
||||
case null: {
|
||||
return GroupCallStatus.Joined;
|
||||
}
|
||||
case GroupCallStatus.Ringing:
|
||||
case GroupCallStatus.Missed:
|
||||
case GroupCallStatus.Declined: {
|
||||
return GroupCallStatus.Accepted;
|
||||
}
|
||||
case GroupCallStatus.GenericGroupCall: {
|
||||
return GroupCallStatus.Joined;
|
||||
}
|
||||
case GroupCallStatus.Joined:
|
||||
case GroupCallStatus.OutgoingRing: {
|
||||
return status;
|
||||
}
|
||||
default: {
|
||||
throw missingCaseError(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.Started) {
|
||||
return GroupCallStatus.GenericGroupCall;
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.Ringing) {
|
||||
return GroupCallStatus.Ringing;
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.Missed) {
|
||||
return GroupCallStatus.Missed;
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.Declined) {
|
||||
return GroupCallStatus.Declined;
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.Hangup) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return GroupCallStatus.Declined;
|
||||
}
|
||||
return GroupCallStatus.Missed;
|
||||
}
|
||||
|
||||
if (event === LocalCallEvent.RemoteHangup) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return GroupCallStatus.Missed;
|
||||
}
|
||||
return GroupCallStatus.Declined;
|
||||
}
|
||||
|
||||
throw missingCaseError(event);
|
||||
}
|
||||
|
||||
// actions
|
||||
// -------
|
||||
|
||||
async function updateLocalCallHistory(
|
||||
callEvent: CallEventDetails,
|
||||
receivedAtCounter: number | null
|
||||
): Promise<CallHistoryDetails | null> {
|
||||
const conversation = window.ConversationController.get(callEvent.peerId);
|
||||
strictAssert(
|
||||
conversation != null,
|
||||
`updateLocalCallHistory: Conversation not found ${formatCallEvent(
|
||||
callEvent
|
||||
)}`
|
||||
);
|
||||
return conversation.queueJob<CallHistoryDetails | null>(
|
||||
'updateLocalCallHistory',
|
||||
async () => {
|
||||
log.info(
|
||||
'updateLocalCallHistory: Processing call event:',
|
||||
formatCallEvent(callEvent)
|
||||
);
|
||||
|
||||
const prevCallHistory =
|
||||
(await window.Signal.Data.getCallHistory(
|
||||
callEvent.callId,
|
||||
callEvent.peerId
|
||||
)) ?? null;
|
||||
|
||||
if (prevCallHistory != null) {
|
||||
log.info(
|
||||
'updateLocalCallHistory: Found previous call history:',
|
||||
formatCallHistory(prevCallHistory)
|
||||
);
|
||||
} else {
|
||||
log.info('updateLocalCallHistory: No previous call history');
|
||||
}
|
||||
|
||||
let callHistory: CallHistoryDetails;
|
||||
try {
|
||||
callHistory = transitionCallHistory(prevCallHistory, callEvent);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
"updateLocalCallHistory: Couldn't transition call history:",
|
||||
formatCallEvent(callEvent),
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info(
|
||||
'updateLocalCallHistory: Saving call history:',
|
||||
formatCallHistory(callHistory)
|
||||
);
|
||||
await window.Signal.Data.saveCallHistory(callHistory);
|
||||
window.reduxActions.callHistory.cacheCallHistory(callHistory);
|
||||
|
||||
const prevMessage =
|
||||
await window.Signal.Data.getCallHistoryMessageByCallId({
|
||||
conversationId: conversation.id,
|
||||
callId: callHistory.callId,
|
||||
});
|
||||
|
||||
if (prevMessage != null) {
|
||||
log.info(
|
||||
'updateLocalCallHistory: Found previous call history message:',
|
||||
prevMessage.id
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
'updateLocalCallHistory: No previous call history message',
|
||||
conversation.id
|
||||
);
|
||||
}
|
||||
|
||||
let unread = false;
|
||||
if (callHistory.mode === CallMode.Direct) {
|
||||
unread =
|
||||
callHistory.direction === CallDirection.Incoming &&
|
||||
callHistory.status === DirectCallStatus.Missed;
|
||||
} else if (callHistory.mode === CallMode.Group) {
|
||||
unread =
|
||||
callHistory.direction === CallDirection.Incoming &&
|
||||
(callHistory.status === GroupCallStatus.GenericGroupCall ||
|
||||
callHistory.status === GroupCallStatus.Missed);
|
||||
}
|
||||
|
||||
let readStatus = unread ? ReadStatus.Unread : ReadStatus.Read;
|
||||
let seenStatus = unread ? SeenStatus.Unseen : SeenStatus.NotApplicable;
|
||||
|
||||
if (prevMessage?.readStatus != null) {
|
||||
readStatus = maxReadStatus(readStatus, prevMessage.readStatus);
|
||||
}
|
||||
if (prevMessage?.seenStatus != null) {
|
||||
seenStatus = maxSeenStatus(seenStatus, prevMessage.seenStatus);
|
||||
}
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
id: prevMessage?.id ?? generateGuid(),
|
||||
conversationId: conversation.id,
|
||||
type: 'call-history',
|
||||
sent_at: callHistory.timestamp,
|
||||
timestamp: callHistory.timestamp,
|
||||
received_at: receivedAtCounter ?? incrementMessageCounter(),
|
||||
received_at_ms: callHistory.timestamp,
|
||||
readStatus,
|
||||
seenStatus,
|
||||
callId: callHistory.callId,
|
||||
};
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(message, {
|
||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||
// We don't want to force save if we're updating an existing message
|
||||
forceSave: prevMessage == null,
|
||||
});
|
||||
log.info('updateLocalCallHistory: Saved call history message:', id);
|
||||
|
||||
const model = window.MessageController.register(
|
||||
id,
|
||||
new window.Whisper.Message({
|
||||
...message,
|
||||
id,
|
||||
})
|
||||
);
|
||||
|
||||
if (callHistory.direction === CallDirection.Outgoing) {
|
||||
conversation.incrementSentMessageCount();
|
||||
} else {
|
||||
conversation.incrementMessageCount();
|
||||
}
|
||||
|
||||
conversation.trigger('newmessage', model);
|
||||
|
||||
void conversation.updateLastMessage();
|
||||
void conversation.updateUnread();
|
||||
conversation.set('active_at', callHistory.timestamp);
|
||||
|
||||
if (canConversationBeUnarchived(conversation.attributes)) {
|
||||
conversation.setArchived(false);
|
||||
} else {
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
}
|
||||
|
||||
return callHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function updateRemoteCallHistory(
|
||||
callHistory: CallHistoryDetails
|
||||
): Promise<void> {
|
||||
if (!shouldSyncStatus(callHistory.status)) {
|
||||
log.info(
|
||||
'updateRemoteCallHistory: Not syncing call history:',
|
||||
formatCallHistory(callHistory)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(
|
||||
'updateRemoteCallHistory: syncing call history:',
|
||||
formatCallHistory(callHistory)
|
||||
);
|
||||
|
||||
try {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
|
||||
syncMessage.callEvent = getProtoForCallHistory(callHistory);
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||
|
||||
await singleProtoJobQueue.add({
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
identifier: myUuid.toString(),
|
||||
isSyncMessage: true,
|
||||
protoBase64: Bytes.toBase64(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
type: 'callEventSync',
|
||||
urgent: false,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'updateRemoteCallHistory: Failed to queue sync message:',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCallHistoryFromRemoteEvent(
|
||||
callEvent: CallEventDetails,
|
||||
receivedAtCounter: number
|
||||
): Promise<void> {
|
||||
await updateLocalCallHistory(callEvent, receivedAtCounter);
|
||||
}
|
||||
|
||||
export async function updateCallHistoryFromLocalEvent(
|
||||
callEvent: CallEventDetails,
|
||||
receivedAtCounter: number | null
|
||||
): Promise<void> {
|
||||
const updatedCallHistory = await updateLocalCallHistory(
|
||||
callEvent,
|
||||
receivedAtCounter
|
||||
);
|
||||
if (updatedCallHistory == null) {
|
||||
return;
|
||||
}
|
||||
await updateRemoteCallHistory(updatedCallHistory);
|
||||
}
|
||||
|
||||
export async function clearCallHistoryDataAndSync(): 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.MessageController.getById(messageId);
|
||||
const conversation = message?.getConversation();
|
||||
if (message == null || conversation == null) {
|
||||
return;
|
||||
}
|
||||
window.reduxActions.conversations.messageDeleted(
|
||||
messageId,
|
||||
message.get('conversationId')
|
||||
);
|
||||
drop(conversation.updateLastMessage());
|
||||
window.MessageController.unregister(messageId);
|
||||
});
|
||||
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const callLogEvent = new Proto.SyncMessage.CallLogEvent({
|
||||
type: Proto.SyncMessage.CallLogEvent.Type.CLEAR,
|
||||
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('clearCallHistory: Queueing sync message');
|
||||
await singleProtoJobQueue.add({
|
||||
contentHint: ContentHint.RESENDABLE,
|
||||
identifier: myUuid.toString(),
|
||||
isSyncMessage: true,
|
||||
protoBase64: Bytes.toBase64(
|
||||
Proto.Content.encode(contentMessage).finish()
|
||||
),
|
||||
type: 'callLogEventSync',
|
||||
urgent: false,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('clearCallHistory: Failed to clear call history', error);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue