diff --git a/protos/SignalService.proto b/protos/SignalService.proto index a8c9b1818..0ec2fbe12 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -228,8 +228,7 @@ message DataMessage { } message GroupCallUpdate { - // Currently just a sentinel message indicating that a client should - // fetch updated group call state. + optional string eraId = 1; } enum ProtocolVersion { diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 021c82d57..93efd416b 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -66,6 +66,16 @@ const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map< [HttpMethod.Delete, 'DELETE'], ]); +// We send group call update messages to tell other clients to peek, which triggers +// notifications, timeline messages, big green "Join" buttons, and so on. This enum +// represents the three possible states we can be in. This helps ensure that we don't +// send an update on disconnect if we never sent one when we joined. +enum GroupCallUpdateMessageState { + SentNothing, + SentJoin, + SentLeft, +} + export { CallState, CanvasVideoRenderer, @@ -379,6 +389,7 @@ export class CallingClass { const groupIdBuffer = base64ToArrayBuffer(groupId); + let updateMessageState = GroupCallUpdateMessageState.SentNothing; let isRequestingMembershipProof = false; const outerGroupCall = RingRTC.getGroupCall( @@ -387,6 +398,7 @@ export class CallingClass { { onLocalDeviceStateChanged: groupCall => { const localDeviceState = groupCall.getLocalDeviceState(); + const { eraId } = groupCall.getPeekInfo() || {}; if ( localDeviceState.connectionState === ConnectionState.NotConnected @@ -397,6 +409,14 @@ export class CallingClass { this.disableLocalCamera(); delete this.callsByConversation[conversationId]; + + if ( + updateMessageState === GroupCallUpdateMessageState.SentJoin && + eraId + ) { + updateMessageState = GroupCallUpdateMessageState.SentLeft; + this.sendGroupCallUpdateMessage(conversationId, eraId); + } } else { this.callsByConversation[conversationId] = groupCall; @@ -406,6 +426,15 @@ export class CallingClass { } else { this.videoCapturer.enableCaptureAndSend(groupCall); } + + if ( + updateMessageState === GroupCallUpdateMessageState.SentNothing && + localDeviceState.joinState === JoinState.Joined && + eraId + ) { + updateMessageState = GroupCallUpdateMessageState.SentJoin; + this.sendGroupCallUpdateMessage(conversationId, eraId); + } } this.syncGroupCallToRedux(conversationId, groupCall); @@ -632,6 +661,35 @@ export class CallingClass { }); } + private sendGroupCallUpdateMessage( + conversationId: string, + eraId: string + ): void { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + window.log.error( + 'Unable to send group call update message for non-existent conversation' + ); + return; + } + + const groupV2 = conversation.getGroupV2Info(); + const sendOptions = conversation.getSendOptions(); + if (!groupV2) { + window.log.error( + 'Unable to send group call update message for conversation that lacks groupV2 info' + ); + return; + } + + // We "fire and forget" because sending this message is non-essential. + window.textsecure.messaging + .sendGroupCallUpdate({ eraId, groupV2 }, sendOptions) + .catch(err => { + window.log.error('Failed to send group call update', err); + }); + } + async accept(conversationId: string, asVideoCall: boolean): Promise { window.log.info('CallingClass.accept()'); diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index a77182e20..824a4fd35 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -653,7 +653,9 @@ export declare namespace DataMessageClass { data?: AttachmentPointerClass; } - class GroupCallUpdate {} + class GroupCallUpdate { + eraId?: string; + } } // Note: we need to use namespaces to express nested classes in Typescript diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 13bd0593d..fadb7379e 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -105,6 +105,10 @@ type GroupV1InfoType = { members: Array; }; +interface GroupCallUpdateType { + eraId: string; +} + type MessageOptionsType = { attachments?: Array | null; body?: string; @@ -125,6 +129,7 @@ type MessageOptionsType = { deletedForEveryoneTimestamp?: number; timestamp: number; mentions?: BodyRangesType; + groupCallUpdate?: GroupCallUpdateType; }; class Message { @@ -180,6 +185,8 @@ class Message { mentions?: BodyRangesType; + groupCallUpdate?: GroupCallUpdateType; + constructor(options: MessageOptionsType) { this.attachments = options.attachments || []; this.body = options.body; @@ -197,6 +204,7 @@ class Message { this.timestamp = options.timestamp; this.deletedForEveryoneTimestamp = options.deletedForEveryoneTimestamp; this.mentions = options.mentions; + this.groupCallUpdate = options.groupCallUpdate; if (!(this.recipients instanceof Array)) { throw new Error('Invalid recipient list'); @@ -386,6 +394,15 @@ class Message { ); } + if (this.groupCallUpdate) { + const { GroupCallUpdate } = window.textsecure.protobuf.DataMessage; + + const groupCallUpdate = new GroupCallUpdate(); + groupCallUpdate.eraId = this.groupCallUpdate.eraId; + + proto.groupCallUpdate = groupCallUpdate; + } + this.dataMessage = proto; return proto; } @@ -1126,6 +1143,20 @@ export default class MessageSender { ); } + async sendGroupCallUpdate( + { groupV2, eraId }: { groupV2: GroupV2InfoType; eraId: string }, + options?: SendOptionsType + ): Promise { + await this.sendMessageToGroup( + { + groupV2, + groupCallUpdate: { eraId }, + timestamp: Date.now(), + }, + options + ); + } + async sendDeliveryReceipt( recipientE164: string, recipientUuid: string, @@ -1630,6 +1661,7 @@ export default class MessageSender { deletedForEveryoneTimestamp, timestamp, mentions, + groupCallUpdate, }: { attachments?: Array; expireTimer?: number; @@ -1644,6 +1676,7 @@ export default class MessageSender { deletedForEveryoneTimestamp?: number; timestamp: number; mentions?: BodyRangesType; + groupCallUpdate?: GroupCallUpdateType; }, options?: SendOptionsType ): Promise { @@ -1695,6 +1728,7 @@ export default class MessageSender { } : undefined, mentions, + groupCallUpdate, }; if (recipients.length === 0) {