Call Disposition

This commit is contained in:
Jamie Kyle 2023-01-09 16:52:01 -08:00 committed by GitHub
parent 9927b132b9
commit e5638c0b20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 445 additions and 53 deletions

View file

@ -988,7 +988,7 @@
"description": "Shown in iOS theme when you or someone quotes to a message which is not from you"
},
"audioPermissionNeeded": {
"message": "To send audio messages, allow Signal Desktop to access your microphone.",
"message": "To send voice messages, allow Signal Desktop to access your microphone.",
"description": "Shown if the user attempts to send an audio message without audio permissions turned on"
},
"audioCallingPermissionNeeded": {
@ -1559,7 +1559,7 @@
"description": "when you block someone and cannot view their video"
},
"calling__block-info": {
"message": "You won't receive their audio or video and they won't receive yours.",
"message": "You won't receive their voice or video and they won't receive yours.",
"description": "Shown in the modal dialog to describe how blocking works in a group call"
},
"calling__overflow__scroll-up": {
@ -2765,15 +2765,23 @@
"description": "Shown in the shortcuts guide"
},
"Keyboard--accept-video-call": {
"message": "Accept call with video",
"description": "Shown in the calling keyboard shortcuts guide"
"message": "Answer call with video",
"description": "(deleted 2023/01/09) Shown in the calling keyboard shortcuts guide"
},
"Keyboard--accept-audio-call": {
"message": "Accept call with audio",
"message": "Answer call with audio",
"description": "(deleted 2023/01/09) Shown in the calling keyboard shortcuts guide"
},
"icu:Keyboard--accept-video-call": {
"messageformat": "Answer call with video (video calls only)",
"description": "Shown in the calling keyboard shortcuts guide"
},
"icu:Keyboard--accept-call-without-video": {
"messageformat": "Answer call without video",
"description": "Shown in the calling keyboard shortcuts guide"
},
"Keyboard--start-audio-call": {
"message": "Start audio call",
"message": "Start voice call",
"description": "Shown in the calling keyboard shortcuts guide"
},
"Keyboard--start-video-call": {
@ -3189,11 +3197,11 @@
"description": "When a user has no common groups, show this warning"
},
"acceptCall": {
"message": "Answer",
"message": "Answer call",
"description": "Shown in tooltip for the button to accept a call (audio or video)"
},
"acceptCallWithoutVideo": {
"message": "Answer without video",
"message": "Answer call without video",
"description": "Shown in tooltip for the button to accept a video call without video"
},
"declineCall": {
@ -3201,40 +3209,40 @@
"description": "Shown in tooltip for the button to decline a call (audio or video)"
},
"declinedIncomingAudioCall": {
"message": "You declined an audio call",
"description": "Shown in conversation history when you declined an incoming audio call"
"message": "You declined a voice call",
"description": "Shown in conversation history when you declined an incoming voice call"
},
"declinedIncomingVideoCall": {
"message": "You declined a video call",
"description": "Shown in conversation history when you declined an incoming video call"
},
"acceptedIncomingAudioCall": {
"message": "Incoming audio call",
"description": "Shown in conversation history when you accepted an incoming audio call"
"message": "Incoming voice call",
"description": "Shown in conversation history when you accepted an incoming voice call"
},
"acceptedIncomingVideoCall": {
"message": "Incoming video call",
"description": "Shown in conversation history when you accepted an incoming video call"
},
"missedIncomingAudioCall": {
"message": "Missed audio call",
"description": "Shown in conversation history when you missed an incoming audio call"
"message": "Missed voice call",
"description": "Shown in conversation history when you missed an incoming voice call"
},
"missedIncomingVideoCall": {
"message": "Missed video call",
"description": "Shown in conversation history when you missed an incoming video call"
},
"acceptedOutgoingAudioCall": {
"message": "Outgoing audio call",
"description": "Shown in conversation history when you made an outgoing audio call"
"message": "Outgoing voice call",
"description": "Shown in conversation history when you made an outgoing voice call"
},
"acceptedOutgoingVideoCall": {
"message": "Outgoing video call",
"description": "Shown in conversation history when you made an outgoing video call"
},
"missedOrDeclinedOutgoingAudioCall": {
"message": "Unanswered audio call",
"description": "Shown in conversation history when your audio call is missed or declined"
"message": "Unanswered voice call",
"description": "Shown in conversation history when your voice call is missed or declined"
},
"missedOrDeclinedOutgoingVideoCall": {
"message": "Unanswered video call",
@ -3249,8 +3257,8 @@
"description": "Shown in a notification body when Signal is minimized to tray"
},
"incomingAudioCall": {
"message": "Incoming audio call...",
"description": "Shown in both the incoming call bar and notification for an incoming audio call"
"message": "Incoming voice call...",
"description": "Shown in both the incoming call bar and notification for an incoming voice call"
},
"incomingVideoCall": {
"message": "Incoming video call...",

View file

@ -573,6 +573,33 @@ message SyncMessage {
optional uint32 registrationId = 3;
}
message CallEvent {
enum Type {
UNKNOWN = 0;
AUDIO_CALL = 1;
VIDEO_CALL = 2;
}
enum Direction {
UNKNOWN = 0;
INCOMING = 1;
OUTGOING = 2;
}
enum Event {
UNKNOWN = 0;
ACCEPTED = 1;
NOT_ACCEPTED = 2;
}
optional bytes peerUuid = 1;
optional uint64 callId = 2;
optional uint64 timestamp = 3;
optional Type type = 4;
optional Direction direction = 5;
optional Event event = 6;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
optional Groups groups = 3;
@ -591,6 +618,7 @@ message SyncMessage {
repeated Viewed viewed = 16;
optional PniIdentity pniIdentity = 17;
optional PniChangeNumber pniChangeNumber = 18;
optional CallEvent callEvent = 19;
}
message AttachmentPointer {

View file

@ -161,6 +161,7 @@ import { clearConversationDraftAttachments } from './util/clearConversationDraft
import { removeLinkPreview } from './services/LinkPreview';
import { PanelType } from './types/Panels';
import { getQuotedMessageSelector } from './state/selectors/composer';
import { onCallEventSync } from './util/onCallEventSync';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -411,6 +412,10 @@ export async function startApp(): Promise<void> {
'storyRecipientUpdate',
queuedEventListener(onStoryRecipientUpdate, false)
);
messageReceiver.addEventListener(
'callEventSync',
queuedEventListener(onCallEventSync, false)
);
});
ourProfileKeyService.initialize(window.storage);

View file

@ -224,8 +224,10 @@ export function IncomingCallBar(props: PropsType): JSX.Element | null {
}, [bounceAppIconStart, bounceAppIconStop]);
const acceptVideoCall = useCallback(() => {
if (isVideoCall) {
acceptCall({ conversationId, asVideoCall: true });
}, [acceptCall, conversationId]);
}
}, [isVideoCall, acceptCall, conversationId]);
const acceptAudioCall = useCallback(() => {
acceptCall({ conversationId, asVideoCall: false });

View file

@ -205,11 +205,11 @@ const CALLING_SHORTCUTS: Array<ShortcutType> = [
keys: [['shift', 'V']],
},
{
description: 'Keyboard--accept-video-call',
description: 'icu:Keyboard--accept-video-call',
keys: [['ctrlOrAlt', 'shift', 'V']],
},
{
description: 'Keyboard--accept-audio-call',
description: 'icu:Keyboard--accept-call-without-video',
keys: [['ctrlOrAlt', 'shift', 'A']],
},
{

View file

@ -18,6 +18,7 @@ import {
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import * as log from '../../logging/log';
import { assertDev } from '../../util/assert';
export type PropsActionsType = {
returnToActiveCall: () => void;
@ -42,11 +43,14 @@ export const CallingNotification: React.FC<PropsType> = React.memo(
let timestamp: number;
let wasMissed = false;
switch (props.callMode) {
case CallMode.Direct:
timestamp = props.acceptedTime || props.endedTime;
case CallMode.Direct: {
const resolvedTime = props.acceptedTime ?? props.endedTime;
assertDev(resolvedTime, 'Direct call must have accepted or ended time');
timestamp = resolvedTime;
wasMissed =
props.wasIncoming && !props.acceptedTime && !props.wasDeclined;
break;
}
case CallMode.Group:
timestamp = props.startedTime;
break;

View file

@ -60,7 +60,7 @@ import type {
} from '../types/Colors';
import type { MessageModel } from './messages';
import { getContact } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { assertDev, strictAssert } from '../util/assert';
import { isConversationMuted } from '../util/isConversationMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
import {
@ -3226,8 +3226,11 @@ export class ConversationModel extends window.Backbone
let detailsToSave: CallHistoryDetailsType;
switch (callHistoryDetails.callMode) {
case CallMode.Direct:
timestamp = callHistoryDetails.endedTime;
case CallMode.Direct: {
const resolvedTime =
callHistoryDetails.acceptedTime ?? callHistoryDetails.endedTime;
assertDev(resolvedTime, 'Direct call must have accepted or ended time');
timestamp = resolvedTime;
unread =
!callHistoryDetails.wasDeclined && !callHistoryDetails.acceptedTime;
detailsToSave = {
@ -3235,6 +3238,7 @@ export class ConversationModel extends window.Backbone
callMode: CallMode.Direct,
};
break;
}
case CallMode.Group:
timestamp = callHistoryDetails.startedTime;
unread = false;
@ -3257,9 +3261,20 @@ export class ConversationModel extends window.Backbone
// TODO: DESKTOP-722
} as unknown as MessageAttributesType;
if (callHistoryDetails.callMode === CallMode.Direct) {
const messageId = await window.Signal.Data.getCallHistoryMessageByCallId(
this.id,
callHistoryDetails.callId
);
if (messageId != null) {
message.id = messageId;
}
}
const id = await window.Signal.Data.saveMessage(message, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
const model = window.MessageController.register(
id,
new window.Whisper.Message({

View file

@ -36,6 +36,7 @@ import {
BandwidthMode,
} from '@signalapp/ringrtc';
import { uniqBy, noop } from 'lodash';
import Long from 'long';
import type {
ActionsType as CallingReduxActionsType,
@ -1549,11 +1550,15 @@ export class CallingClass {
await this.handleOutgoingSignaling(remoteUserId, message);
const callId = callingMessage.offer.callId?.toString();
assertDev(callId != null, 'Call ID missing from offer');
const ProtoOfferType = Proto.CallingMessage.Offer.Type;
this.addCallHistoryForFailedIncomingCall(
await this.addCallHistoryForFailedIncomingCall(
conversation,
callingMessage.offer.type === ProtoOfferType.OFFER_VIDEO_CALL,
envelope.timestamp
envelope.timestamp,
callId
);
return;
@ -1847,6 +1852,7 @@ export class CallingClass {
return null;
}
const callId = Long.fromValue(call.callId).toString();
try {
// The peer must be 'trusted' before accepting a call from them.
// This is mostly the safety number check, unverified meaning that they were
@ -1859,10 +1865,11 @@ export class CallingClass {
log.info(
`Peer is not trusted, ignoring incoming call for conversation: ${conversation.idForLogging()}`
);
this.addCallHistoryForFailedIncomingCall(
await this.addCallHistoryForFailedIncomingCall(
conversation,
call.isVideoCall,
Date.now()
Date.now(),
callId
);
return null;
}
@ -1879,17 +1886,18 @@ export class CallingClass {
return await this.getCallSettings(conversation);
} catch (err) {
log.error(`Ignoring incoming call: ${Errors.toLogFormat(err)}`);
this.addCallHistoryForFailedIncomingCall(
await this.addCallHistoryForFailedIncomingCall(
conversation,
call.isVideoCall,
Date.now()
Date.now(),
callId
);
return null;
}
}
private handleAutoEndedIncomingCallRequest(
_callId: CallId,
private async handleAutoEndedIncomingCallRequest(
callId: CallId,
remoteUserId: UserId,
reason: CallEndedReason,
ageInSeconds: number,
@ -1909,12 +1917,13 @@ export class CallingClass {
: 0;
const endedTime = Date.now() - ageInMilliseconds;
this.addCallHistoryForAutoEndedIncomingCall(
await this.addCallHistoryForAutoEndedIncomingCall(
conversation,
reason,
endedTime,
wasVideoCall,
receivedAtCounter
receivedAtCounter,
Long.fromValue(callId).toString()
);
}
@ -1929,16 +1938,18 @@ export class CallingClass {
let acceptedTime: number | undefined;
// eslint-disable-next-line no-param-reassign
call.handleStateChanged = () => {
call.handleStateChanged = async () => {
if (call.state === CallState.Accepted) {
acceptedTime = acceptedTime || Date.now();
} else if (call.state === CallState.Ended) {
this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
await this.addCallHistoryForEndedCall(conversation, call, acceptedTime);
this.stopDeviceReselectionTimer();
this.lastMediaDeviceSettings = undefined;
delete this.callsByConversation[conversation.id];
}
reduxInterface.callStateChange({
remoteUserId: call.remoteUserId,
callId: Long.fromValue(call.callId).toString(),
conversationId: conversation.id,
acceptedTime,
callState: call.state,
@ -2088,7 +2099,7 @@ export class CallingClass {
};
}
private addCallHistoryForEndedCall(
private async addCallHistoryForEndedCall(
conversation: ConversationModel,
call: Call,
acceptedTimeParam: number | undefined
@ -2110,8 +2121,11 @@ export class CallingClass {
acceptedTime = Date.now();
}
void conversation.addCallHistory(
const callId = Long.fromValue(call.callId).toString();
await conversation.addCallHistory(
{
callId,
callMode: CallMode.Direct,
wasIncoming: call.isIncoming,
wasVideoCall: call.isVideoCall,
@ -2123,12 +2137,13 @@ export class CallingClass {
);
}
private addCallHistoryForFailedIncomingCall(
private async addCallHistoryForFailedIncomingCall(
conversation: ConversationModel,
wasVideoCall: boolean,
timestamp: number
timestamp: number,
callId: string
) {
void conversation.addCallHistory(
await conversation.addCallHistory(
{
callMode: CallMode.Direct,
wasIncoming: true,
@ -2137,17 +2152,19 @@ export class CallingClass {
wasDeclined: false,
acceptedTime: undefined,
endedTime: timestamp,
callId,
},
undefined
);
}
private addCallHistoryForAutoEndedIncomingCall(
private async addCallHistoryForAutoEndedIncomingCall(
conversation: ConversationModel,
reason: CallEndedReason,
endedTime: number,
wasVideoCall: boolean,
receivedAtCounter: number | undefined
receivedAtCounter: number | undefined,
callId: string
) {
let wasDeclined = false;
let acceptedTime;
@ -2159,8 +2176,9 @@ export class CallingClass {
}
// Otherwise it will show up as a missed call.
void conversation.addCallHistory(
await conversation.addCallHistory(
{
callId,
callMode: CallMode.Direct,
wasIncoming: true,
wasVideoCall,

View file

@ -546,6 +546,10 @@ export type DataInterface = {
getLastConversationMessage(options: {
conversationId: string;
}): Promise<MessageType | undefined>;
getCallHistoryMessageByCallId(
conversationId: string,
callId: string
): Promise<string | void>;
hasGroupCallHistoryMessage: (
conversationId: string,
eraId: string

View file

@ -256,6 +256,7 @@ const dataInterface: ServerInterface = {
getConversationRangeCenteredOnMessage,
getConversationMessageStats,
getLastConversationMessage,
getCallHistoryMessageByCallId,
hasGroupCallHistoryMessage,
migrateConversationMessages,
@ -3009,6 +3010,32 @@ async function getConversationRangeCenteredOnMessage({
})();
}
async function getCallHistoryMessageByCallId(
conversationId: string,
callId: string
): Promise<string | void> {
const db = getInstance();
const id: string | void = db
.prepare<Query>(
`
SELECT id
FROM messages
WHERE conversationId = $conversationId
AND type = 'call-history'
AND callMode = 'Direct'
AND callId = $callId
`
)
.pluck()
.get({
conversationId,
callId,
});
return id;
}
async function hasGroupCallHistoryMessage(
conversationId: string,
eraId: string

View file

@ -0,0 +1,38 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion72(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 72) {
return;
}
db.transaction(() => {
db.exec(
`
ALTER TABLE messages
ADD COLUMN callId TEXT
GENERATED ALWAYS AS (
json_extract(json, '$.callHistoryDetails.callId')
);
ALTER TABLE messages
ADD COLUMN callMode TEXT
GENERATED ALWAYS AS (
json_extract(json, '$.callHistoryDetails.callMode')
);
CREATE INDEX messages_call ON messages
(conversationId, type, callMode, callId);
`
);
db.pragma('user_version = 72');
})();
logger.info('updateToSchemaVersion72: success!');
}

View file

@ -47,6 +47,7 @@ import updateToSchemaVersion68 from './68-drop-deprecated-columns';
import updateToSchemaVersion69 from './69-group-call-ring-cancellations';
import updateToSchemaVersion70 from './70-story-reply-index';
import updateToSchemaVersion71 from './71-merge-notifications';
import updateToSchemaVersion72 from './72-optimize-call-id-message-lookup';
function updateToSchemaVersion1(
currentVersion: number,
@ -1963,6 +1964,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion70,
updateToSchemaVersion71,
updateToSchemaVersion72,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View file

@ -52,6 +52,8 @@ import { isDirectConversation } from '../../util/whatTypeOfConversation';
import { SHOW_TOAST } from './toast';
import { ToastType } from '../../types/Toast';
import type { ShowToastActionType } from './toast';
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
import MessageSender from '../../textsecure/SendMessage';
// State
@ -137,6 +139,8 @@ export type AcceptCallType = {
};
export type CallStateChangeType = {
remoteUserId: string; // TODO: Remove
callId: string; // TODO: Remove
conversationId: string;
acceptedTime?: number;
callState: CallState;
@ -688,12 +692,67 @@ function callStateChange(
CallStateChangeFulfilledActionType
> {
return async dispatch => {
const { callState } = payload;
const {
callId,
callState,
isVideoCall,
isIncoming,
acceptedTime,
callEndedReason,
remoteUserId,
} = payload;
if (callState === CallState.Ended) {
await callingTones.playEndCall();
ipcRenderer.send('close-screen-share-controller');
}
const isOutgoing = !isIncoming;
const wasAccepted = acceptedTime != null;
const isConnected = callState === CallState.Accepted; // "connected"
const isEnded = callState === CallState.Ended && callEndedReason != null;
const isLocalHangup = callEndedReason === CallEndedReason.LocalHangup;
const isRemoteHangup = callEndedReason === CallEndedReason.RemoteHangup;
const answered = isConnected && wasAccepted;
const notAnswered = isEnded && !wasAccepted;
const isOutgoingRemoteAccept = isOutgoing && isConnected && answered;
const isIncomingLocalAccept = isIncoming && isConnected && answered;
const isOutgoingLocalHangup = isOutgoing && isLocalHangup && notAnswered;
const isIncomingLocalHangup = isIncoming && isLocalHangup && notAnswered;
const isOutgoingRemoteHangup = isOutgoing && isRemoteHangup && notAnswered;
const isIncomingRemoteHangup = isIncoming && isRemoteHangup && notAnswered;
if (isIncomingRemoteHangup) {
// This is considered just another "missed" event
log.info('callStateChange: not syncing hangup from self');
} else if (
isOutgoingRemoteAccept ||
isIncomingLocalAccept ||
isOutgoingLocalHangup ||
isIncomingLocalHangup ||
isOutgoingRemoteHangup
) {
try {
await singleProtoJobQueue.add(
MessageSender.getCallEventSync(
remoteUserId,
callId,
isVideoCall,
isIncoming,
acceptedTime != null
)
);
} catch (error) {
log.error(
'callStateChange: Failed to queue sync message',
Errors.toLogFormat(error)
);
}
}
dispatch({
type: CALL_STATE_CHANGE_FULFILLED,
payload,

View file

@ -37,7 +37,7 @@ import {
SignedPreKeys,
} from '../LibSignalStores';
import { verifySignature } from '../Curve';
import { strictAssert } from '../util/assert';
import { assertDev, strictAssert } from '../util/assert';
import type { BatcherType } from '../util/batcher';
import { createBatcher } from '../util/batcher';
import { drop } from '../util/drop';
@ -111,6 +111,7 @@ import {
GroupEvent,
GroupSyncEvent,
StoryRecipientUpdateEvent,
CallEventSyncEvent,
} from './messageReceiverEvents';
import * as log from '../logging/log';
import * as durations from '../util/durations';
@ -617,6 +618,11 @@ export default class MessageReceiver
handler: (ev: StoryRecipientUpdateEvent) => void
): void;
public override addEventListener(
name: 'callEventSync',
handler: (ev: CallEventSyncEvent) => void
): void;
public override addEventListener(name: string, handler: EventHandler): void {
return super.addEventListener(name, handler);
}
@ -2958,6 +2964,9 @@ export default class MessageReceiver
if (syncMessage.viewed && syncMessage.viewed.length) {
return this.handleViewed(envelope, syncMessage.viewed);
}
if (syncMessage.callEvent) {
return this.handleCallEvent(envelope, syncMessage.callEvent);
}
this.removeFromCache(envelope);
log.warn(
@ -3219,6 +3228,64 @@ export default class MessageReceiver
);
}
private async handleCallEvent(
envelope: ProcessedEnvelope,
callEvent: Proto.SyncMessage.ICallEvent
): Promise<void> {
const logId = getEnvelopeId(envelope);
log.info('MessageReceiver.handleCallEvent', logId);
const { peerUuid, callId } = callEvent;
if (!peerUuid) {
throw new Error('MessageReceiver.handleCallEvent: missing peerUuid');
}
if (!callId) {
throw new Error('MessageReceiver.handleCallEvent: missing callId');
}
logUnexpectedUrgentValue(envelope, 'callEventSync');
const peerUuidStr = bytesToUuid(peerUuid);
assertDev(
peerUuidStr != null,
'MessageReceiver.handleCallEvent: invalid peerUuid'
);
const { receivedAtCounter, timestamp } = envelope;
const wasIncoming =
callEvent.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING;
const wasVideoCall =
callEvent.type === Proto.SyncMessage.CallEvent.Type.VIDEO_CALL;
const wasAccepted =
callEvent.event === Proto.SyncMessage.CallEvent.Event.ACCEPTED;
const wasDeclined =
callEvent.event === Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED;
const acceptedTime = wasAccepted ? timestamp : undefined;
const endedTime = wasDeclined ? timestamp : undefined;
const callEventSync = new CallEventSyncEvent(
{
timestamp: envelope.timestamp,
peerUuid: peerUuidStr,
callId: callId.toString(),
wasIncoming,
wasVideoCall,
wasDeclined,
acceptedTime,
endedTime,
receivedAtCounter,
},
this.removeFromCache.bind(this, envelope)
);
await this.dispatchAndWait(logId, callEventSync);
log.info('handleCallEvent: finished');
}
private async handleContacts(
envelope: ProcessedEnvelope,
contacts: Proto.SyncMessage.IContacts

View file

@ -1897,6 +1897,52 @@ export default class MessageSender {
};
}
static getCallEventSync(
peerUuid: string,
callId: string,
isVideoCall: boolean,
isIncoming: boolean,
isAccepted: boolean
): SingleProtoJobData {
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const syncMessage = MessageSender.createSyncMessage();
const type = isVideoCall
? Proto.SyncMessage.CallEvent.Type.VIDEO_CALL
: Proto.SyncMessage.CallEvent.Type.AUDIO_CALL;
const direction = isIncoming
? Proto.SyncMessage.CallEvent.Direction.INCOMING
: Proto.SyncMessage.CallEvent.Direction.OUTGOING;
const event = isAccepted
? Proto.SyncMessage.CallEvent.Event.ACCEPTED
: Proto.SyncMessage.CallEvent.Event.NOT_ACCEPTED;
syncMessage.callEvent = new Proto.SyncMessage.CallEvent({
peerUuid: uuidToBytes(peerUuid),
callId: Long.fromString(callId),
type,
direction,
event,
timestamp: Long.fromNumber(Date.now()),
});
const contentMessage = new Proto.Content();
contentMessage.syncMessage = syncMessage;
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return {
contentHint: ContentHint.RESENDABLE,
identifier: myUuid.toString(),
isSyncMessage: true,
protoBase64: Bytes.toBase64(
Proto.Content.encode(contentMessage).finish()
),
type: 'callEventSync',
urgent: false,
};
}
static getVerificationSync(
destinationE164: string | undefined,
destinationUuid: string | undefined,

View file

@ -404,6 +404,27 @@ export class ViewSyncEvent extends ConfirmableEvent {
}
}
export type CallEventSyncEventData = Readonly<{
timestamp: number;
peerUuid: string;
callId: string;
wasVideoCall: boolean;
wasIncoming: boolean;
wasDeclined: boolean;
acceptedTime: number | undefined;
endedTime: number | undefined;
receivedAtCounter: number;
}>;
export class CallEventSyncEvent extends ConfirmableEvent {
constructor(
public readonly callEvent: CallEventSyncEventData,
confirm: ConfirmCallback
) {
super('callEventSync', confirm);
}
}
export type StoryRecipientUpdateData = Readonly<{
destinationUuid: string;
storyMessageRecipients: Array<Proto.SyncMessage.Sent.IStoryMessageRecipient>;

View file

@ -167,12 +167,13 @@ export type MediaDeviceSettings = AvailableIODevicesType & {
};
type DirectCallHistoryDetailsType = {
callId: string;
callMode: CallMode.Direct;
wasIncoming: boolean;
wasVideoCall: boolean;
wasDeclined: boolean;
acceptedTime?: number;
endedTime: number;
endedTime?: number;
};
type GroupCallHistoryDetailsType = {

View file

@ -13,7 +13,7 @@ type DirectCallNotificationType = {
wasVideoCall: boolean;
wasDeclined: boolean;
acceptedTime?: number;
endedTime: number;
endedTime?: number;
};
type GroupCallNotificationType = {

View file

@ -65,6 +65,7 @@ export const sendTypesEnum = z.enum([
'verificationSync',
'viewOnceSync',
'viewSync',
'callEventSync',
// No longer used, all non-urgent
'legacyGroupChange',

View file

@ -0,0 +1,46 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CallEventSyncEvent } from '../textsecure/messageReceiverEvents';
import * as log from '../logging/log';
import { CallMode } from '../types/Calling';
export async function onCallEventSync(
syncEvent: CallEventSyncEvent
): Promise<void> {
const { callEvent, confirm } = syncEvent;
const {
peerUuid,
callId,
wasIncoming,
wasVideoCall,
wasDeclined,
acceptedTime,
endedTime,
receivedAtCounter,
} = callEvent;
const conversation = window.ConversationController.get(peerUuid);
if (!conversation) {
log.warn(`onCallEventSync: No conversation found for peerUuid ${peerUuid}`);
return;
}
await conversation.queueJob('onCallEventSync', async () => {
await conversation.addCallHistory(
{
callId,
callMode: CallMode.Direct,
wasDeclined,
wasIncoming,
wasVideoCall,
acceptedTime: acceptedTime ?? undefined,
endedTime: endedTime ?? undefined,
},
receivedAtCounter
);
confirm();
});
}