diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 73894ada6c6..cf4f19bd356 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -602,6 +602,7 @@ message SyncMessage { ACCEPTED = 1; NOT_ACCEPTED = 2; DELETE = 3; + OBSERVED = 4; } /* Data identifying a conversation. The service ID for 1:1, the group ID for diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 0a3014f6930..a3c0ed85e82 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -3189,22 +3189,25 @@ export class CallingClass { const callId = getCallIdFromEra(peekInfo.eraId); try { - // We only log events confirmed joined. If admin approval is required, then - // the call begins in the Pending state and we don't want history for that. - if (joinState !== GroupCallJoinState.Joined) { + let localCallEvent; + if (joinState === GroupCallJoinState.Joined) { + localCallEvent = LocalCallEvent.Accepted; + } else if (peekInfo && peekInfo.devices.length > 0) { + localCallEvent = LocalCallEvent.Started; + } else { return; } const callDetails = getCallDetailsForAdhocCall(roomId, callId); const callEvent = getCallEventDetails( callDetails, - LocalCallEvent.Accepted, - 'CallingClass.updateCallHistoryForGroupCallOnLocalChanged' + localCallEvent, + 'CallingClass.updateCallHistoryForAdhocCall' ); await updateAdhocCallHistory(callEvent); } catch (error) { log.error( - 'CallingClass.updateCallHistoryForGroupCallOnLocalChanged: Error updating state', + 'CallingClass.updateCallHistoryForAdhocCall: Error updating state', Errors.toLogFormat(error) ); } diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 97d4b4f2817..a6255d5cc4f 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -536,11 +536,10 @@ const doGroupCallPeek = ({ log.info(`doGroupCallPeek/${logId}: Found ${peekInfo.deviceCount} devices`); + const joinState = isGroupOrAdhocCallState(existingCall) + ? existingCall.joinState + : null; if (callMode === CallMode.Group) { - const joinState = isGroupOrAdhocCallState(existingCall) - ? existingCall.joinState - : null; - try { await calling.updateCallHistoryForGroupCallOnPeek( conversationId, @@ -555,6 +554,12 @@ const doGroupCallPeek = ({ } dispatch(updateLastMessage(conversationId)); + } else if (callMode === CallMode.Adhoc) { + await calling.updateCallHistoryForAdhocCall( + conversationId, + joinState, + peekInfo + ); } const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux(peekInfo); diff --git a/ts/types/CallDisposition.ts b/ts/types/CallDisposition.ts index a4f96b9ff9f..9ec61943d57 100644 --- a/ts/types/CallDisposition.ts +++ b/ts/types/CallDisposition.ts @@ -44,6 +44,7 @@ export enum RemoteCallEvent { Accepted = 'Accepted', NotAccepted = 'NotAccepted', Delete = 'Delete', + Observed = 'Observed', } export type CallEvent = LocalCallEvent | RemoteCallEvent; @@ -55,6 +56,7 @@ export enum CallStatusValue { Declined = 'Declined', Deleted = 'Deleted', GenericGroupCall = 'GenericGroupCall', + GenericAdhocCall = 'GenericAdhocCall', OutgoingRing = 'OutgoingRing', Ringing = 'Ringing', Joined = 'Joined', @@ -81,6 +83,7 @@ export enum GroupCallStatus { } export enum AdhocCallStatus { + Generic = CallStatusValue.GenericAdhocCall, Pending = CallStatusValue.Pending, Joined = CallStatusValue.JoinedAdhoc, Deleted = CallStatusValue.Deleted, diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index ed043d6a16b..1595a11f81a 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -67,6 +67,7 @@ import { import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationModel } from '../models/conversations'; import { drop } from './drop'; +import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync'; // utils // ----- @@ -234,6 +235,8 @@ export function getCallEventForProto( event = RemoteCallEvent.NotAccepted; } else if (callEvent.event === Proto.SyncMessage.CallEvent.Event.DELETE) { event = RemoteCallEvent.Delete; + } else if (callEvent.event === Proto.SyncMessage.CallEvent.Event.OBSERVED) { + event = RemoteCallEvent.Observed; } else { throw new TypeError(`Unknown call event ${callEvent.event}`); } @@ -301,6 +304,8 @@ const statusToProto: Record< [CallStatusValue.Missed]: null, [CallStatusValue.Pending]: null, [CallStatusValue.GenericGroupCall]: null, + [CallStatusValue.GenericAdhocCall]: + Proto.SyncMessage.CallEvent.Event.OBSERVED, [CallStatusValue.OutgoingRing]: null, [CallStatusValue.Ringing]: null, [CallStatusValue.Joined]: null, @@ -650,6 +655,15 @@ function transitionTimestamp( return callHistory.timestamp; } + // Observed call history should only be changed if we get a remote observed + // event with possibly a better timestamp. + if (callHistory.status === AdhocCallStatus.Generic) { + if (callEvent.event === RemoteCallEvent.Observed) { + return latestTimestamp; + } + return callHistory.timestamp; + } + // Declined call history should only be changed if if we transition to an // accepted state or get a remote 'not accepted' event with possibly a better // timestamp. @@ -713,6 +727,12 @@ function transitionDirectCallStatus( return DirectCallStatus.Missed; } + if (callEvent === RemoteCallEvent.Observed) { + throw new Error( + `callHistoryDetails: Direct calls shouldn't send ${callEvent}` + ); + } + if (callEvent === LocalCallEvent.Missed) { return DirectCallStatus.Missed; } @@ -766,7 +786,10 @@ function transitionGroupCallStatus( return status; } - if (event === RemoteCallEvent.NotAccepted) { + if ( + event === RemoteCallEvent.NotAccepted || + event === RemoteCallEvent.Observed + ) { throw new Error(`callHistoryDetails: Group calls shouldn't send ${event}`); } @@ -859,6 +882,13 @@ function transitionAdhocCallStatus( return status; } + if ( + callEvent === RemoteCallEvent.Observed || + callEvent === LocalCallEvent.Started + ) { + return AdhocCallStatus.Generic; + } + // For Adhoc calls, ringing and corresponding events are not supported currently. // However we handle those events here to be exhaustive. if ( @@ -867,7 +897,6 @@ function transitionAdhocCallStatus( callEvent === LocalCallEvent.Declined || callEvent === LocalCallEvent.Hangup || callEvent === LocalCallEvent.RemoteHangup || - callEvent === LocalCallEvent.Started || // never actually happens, but need for exhaustive check callEvent === LocalCallEvent.Ringing ) { @@ -985,7 +1014,8 @@ export async function updateLocalAdhocCallHistory( } strictAssert( - callHistory.status === AdhocCallStatus.Pending || + callHistory.status === AdhocCallStatus.Generic || + callHistory.status === AdhocCallStatus.Pending || callHistory.status === AdhocCallStatus.Joined || callHistory.status === AdhocCallStatus.Deleted, `updateAdhocCallHistory: callHistory must have adhoc status (was ${callHistory.status})` @@ -1002,6 +1032,28 @@ export async function updateLocalAdhocCallHistory( formatCallHistory(callHistory) ); await window.Signal.Data.saveCallHistory(callHistory); + + /* + If we're not a call link admin and this is the first call history for this link, + then it means we clicked someone else's link and discovered it was active. We + sync this newly observed call link so the subsequent call event OBSERVED sync + message refers to a valid call link. + */ + if (prevCallHistory == null) { + const callLink = await window.Signal.Data.getCallLinkByRoomId( + callEvent.peerId + ); + if (callLink) { + log.info( + `updateAdhocCallHistory: Syncing new observed call link ${callEvent.peerId}` + ); + drop(sendCallLinkUpdateSync(callLink)); + } else { + log.error( + `updateAdhocCallHistory: New observed call link missing in DB: ${callEvent.peerId}` + ); + } + } } if (isDeleted) {