Skip to content

Commit

Permalink
chore: cherry-pick MLS 1:1 conversation over SFT to dev [WPB-10120] (#…
Browse files Browse the repository at this point in the history
…18066)

* runfix: treat 1:1 calls as conference calls (#17759)

* fix: handle leaving a 1:1 call over SFT [WPB-7151] (#17883)

* feat: establish 1on1 mls conversation with new removal key [WPB-10744] (#18062)

* chore: use backend config for 1 on 1 SFT calls (#18063)

* chore: address remaining merge conflicts

* Update src/script/calling/CallingRepository.ts

Co-authored-by: Przemysław Jóźwik <[email protected]>

* chore: show quality feedback modal if countly enabled

* chore: bump core to 46.4.0

---------

Co-authored-by: Patryk Górka <[email protected]>
Co-authored-by: Przemysław Jóźwik <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 0b3855a commit d89b2a9
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 66 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@mediapipe/tasks-vision": "0.10.15",
"@wireapp/avs": "9.9.3",
"@wireapp/commons": "5.2.10",
"@wireapp/core": "46.3.2",
"@wireapp/core": "46.4.0",
"@wireapp/react-ui-kit": "9.23.4",
"@wireapp/store-engine-dexie": "2.1.12",
"@wireapp/webapp-events": "0.24.0",
Expand Down
129 changes: 107 additions & 22 deletions src/script/calling/CallingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import type {CallConfigData} from '@wireapp/api-client/lib/account/CallConfigData';
import {QualifiedUserClients} from '@wireapp/api-client/lib/conversation';
import {FEATURE_KEY} from '@wireapp/api-client/lib/team';
import type {QualifiedId} from '@wireapp/api-client/lib/user';
import type {WebappProperties} from '@wireapp/api-client/lib/user/data';
import {MessageSendingState} from '@wireapp/core/lib/conversation';
Expand Down Expand Up @@ -71,7 +72,7 @@ import {ClientId, Participant, UserId} from './Participant';

import {PrimaryModal} from '../components/Modals/PrimaryModal';
import {Config} from '../Config';
import {isGroupMLSConversation, isMLSConversation, MLSConversation} from '../conversation/ConversationSelectors';
import {isMLSConversation, MLSConversation} from '../conversation/ConversationSelectors';
import {ConversationState} from '../conversation/ConversationState';
import {ConversationVerificationState} from '../conversation/ConversationVerificationState';
import {EventBuilder} from '../conversation/EventBuilder';
Expand Down Expand Up @@ -104,7 +105,9 @@ interface MediaStreamQuery {
screen?: boolean;
}

export type QualifiedWcallMember = Omit<WcallMember, 'userid'> & {userId: QualifiedId};
export type QualifiedWcallMember = Omit<WcallMember, 'userid'> & {
userId: QualifiedId;
};

interface SendMessageTarget {
clients: WcallClient[];
Expand All @@ -128,7 +131,11 @@ enum CALL_DIRECTION {
OUTGOING = 'outgoing',
}

type SubconversationData = {epoch: number; secretKey: string; members: SubconversationEpochInfoMember[]};
type SubconversationData = {
epoch: number;
secretKey: string;
members: SubconversationEpochInfoMember[];
};

export class CallingRepository {
private readonly acceptVersionWarning: (conversationId: QualifiedId) => void;
Expand Down Expand Up @@ -410,6 +417,10 @@ export class CallingRepository {
activeCall?.muteState(isMuted ? this.nextMuteState : MuteState.NOT_MUTED);
};

private readonly isMLSConference = (conversation: Conversation): conversation is MLSConversation => {
return isMLSConversation(conversation) && this.getConversationType(conversation) === CONV_TYPE.CONFERENCE_MLS;
};

public async pushClients(call: Call | undefined = this.callState.joinedCall(), checkMismatch?: boolean) {
if (!call) {
return false;
Expand All @@ -418,12 +429,15 @@ export class CallingRepository {

const allClients = await this.core.service!.conversation.fetchAllParticipantsClients(conversation.qualifiedId);

if (!isGroupMLSConversation(conversation)) {
if (!this.isMLSConference(conversation)) {
const qualifiedClients = flattenUserMap(allClients);

const clients: Clients = flatten(
qualifiedClients.map(({data, userId}) =>
data.map(clientid => ({clientid, userid: this.serializeQualifiedId(userId)})),
data.map(clientid => ({
clientid,
userid: this.serializeQualifiedId(userId),
})),
),
);

Expand Down Expand Up @@ -659,7 +673,10 @@ export class CallingRepository {
private extractTargetedConversationId(event: CallingEvent): QualifiedId {
const {targetConversation, conversation, qualified_conversation} = event;
const targetedConversationId = targetConversation || qualified_conversation;
const conversationId = targetedConversationId ?? {domain: '', id: conversation};
const conversationId = targetedConversationId ?? {
domain: '',
id: conversation,
};

return conversationId;
}
Expand Down Expand Up @@ -808,7 +825,7 @@ export class CallingRepository {
this.serializeQualifiedId(conversation.qualifiedId),
this.serializeQualifiedId(userId),
conversation && isMLSConversation(conversation) ? senderClientId : clientId,
conversation && isGroupMLSConversation(conversation) ? CONV_TYPE.CONFERENCE_MLS : CONV_TYPE.CONFERENCE,
conversation && this.getConversationType(conversation),
);

if (res !== 0) {
Expand All @@ -830,14 +847,18 @@ export class CallingRepository {
//##############################################################################

private getConversationType(conversation: Conversation): CONV_TYPE {
if (!conversation.isGroup()) {
return CONV_TYPE.ONEONONE;
}
const useSFTForOneToOneCalls =
this.teamState.teamFeatures()?.[FEATURE_KEY.CONFERENCE_CALLING]?.config?.useSFTForOneToOneCalls;

if (isGroupMLSConversation(conversation)) {
return CONV_TYPE.CONFERENCE_MLS;
if (conversation.isGroup() || useSFTForOneToOneCalls) {
if (isMLSConversation(conversation)) {
return CONV_TYPE.CONFERENCE_MLS;
}

return this.supportsConferenceCalling ? CONV_TYPE.CONFERENCE : CONV_TYPE.GROUP;
}
return this.supportsConferenceCalling ? CONV_TYPE.CONFERENCE : CONV_TYPE.GROUP;

return CONV_TYPE.ONEONONE;
}

async startCall(conversation: Conversation, callType: CALL_TYPE): Promise<void | Call> {
Expand Down Expand Up @@ -895,7 +916,7 @@ export class CallingRepository {
this.removeCall(call);
}

if (isGroupMLSConversation(conversation)) {
if (this.isMLSConference(conversation)) {
await this.joinMlsConferenceSubconversation(conversation);
}

Expand Down Expand Up @@ -1033,7 +1054,7 @@ export class CallingRepository {
[Segmentation.CALL.DIRECTION]: this.getCallDirection(call),
});

if (!conversation || !isGroupMLSConversation(conversation)) {
if (!conversation || !this.isMLSConference(conversation)) {
return;
}

Expand All @@ -1050,6 +1071,18 @@ export class CallingRepository {
return this.conversationState.findConversation(conversationId);
}

private readonly leave1on1MLSConference = async (conversationId: QualifiedId) => {
if (isCountlyEnabledAtCurrentEnvironment()) {
this.showCallQualityFeedbackModal();
}

await this.subconversationService.leaveConferenceSubconversation(conversationId);

const conversationIdStr = this.serializeQualifiedId(conversationId);
this.wCall?.end(this.wUser, conversationIdStr);
callingSubscriptions.removeCall(conversationId);
};

private readonly leaveMLSConference = async (conversationId: QualifiedId) => {
await this.subconversationService.leaveConferenceSubconversation(conversationId);
};
Expand All @@ -1067,7 +1100,7 @@ export class CallingRepository {

private readonly updateConferenceSubconversationEpoch = async (conversationId: QualifiedId) => {
const conversation = this.getConversationById(conversationId);
if (!conversation || !isGroupMLSConversation(conversation)) {
if (!conversation || !this.isMLSConference(conversation)) {
return;
}

Expand All @@ -1086,7 +1119,7 @@ export class CallingRepository {

private readonly handleCallParticipantChange = (conversationId: QualifiedId, members: QualifiedWcallMember[]) => {
const conversation = this.getConversationById(conversationId);
if (!conversation || !isGroupMLSConversation(conversation)) {
if (!conversation || !this.isMLSConference(conversation)) {
return;
}

Expand Down Expand Up @@ -1455,7 +1488,9 @@ export class CallingRepository {
};

readonly sendInCallEmoji = async (emojis: string, call: Call) => {
void this.messageRepository.sendInCallEmoji(call.conversation, {[emojis]: 1});
void this.messageRepository.sendInCallEmoji(call.conversation, {
[emojis]: 1,
});
};

readonly sendModeratorMute = (conversationId: QualifiedId, participants: Participant[]) => {
Expand Down Expand Up @@ -1513,22 +1548,30 @@ export class CallingRepository {
Warnings.hideWarning(Warnings.TYPE.CALL_QUALITY_POOR);
const conversationId = this.parseQualifiedId(convId);
const call = this.findCall(conversationId);
const conversation = this.getConversationById(conversationId);
if (!call) {
return;
}

if (
matchQualifiedIds(
call.conversation.qualifiedId,
this.callState.detachedWindowCallQualifiedId() ?? {id: '', domain: ''},
this.callState.detachedWindowCallQualifiedId() ?? {
id: '',
domain: '',
},
)
) {
void this.callState.setViewModeMinimized();
}

// There's nothing we need to do for non-mls calls
if (call.conversationType === CONV_TYPE.CONFERENCE_MLS) {
await this.leaveMLSConference(conversationId);
if (!conversation?.is1to1()) {
await this.leaveMLSConference(conversationId);
} else {
await this.leave1on1MLSConference(conversationId);
}
}

// Remove all the tasks related to the call
Expand Down Expand Up @@ -1655,7 +1698,11 @@ export class CallingRepository {
if (!conversation || !this.selfUser || !this.selfClientId) {
this.logger.warn(
'Unable to process incoming call',
JSON.stringify({conversationId, selfClientId: this.selfClientId, selfUser: this.selfUser}),
JSON.stringify({
conversationId,
selfClientId: this.selfClientId,
selfUser: this.selfUser,
}),
);
return;
}
Expand Down Expand Up @@ -1779,13 +1826,51 @@ export class CallingRepository {
userId: this.parseQualifiedId(member.userid),
}));

this.handleOneToOneMlsCallParticipantLeave(conversationId, members);
this.updateParticipantList(call, members);
this.updateParticipantMutedState(call, members);
this.updateParticipantVideoState(call, members);
this.updateParticipantAudioState(call, members);
this.handleCallParticipantChange(conversationId, members);
};

private readonly handleOneToOneMlsCallParticipantLeave = (
conversationId: QualifiedId,
members: QualifiedWcallMember[],
) => {
const conversation = this.getConversationById(conversationId);
const call = this.findCall(conversationId);

if (!conversation || !this.isMLSConference(conversation) || !conversation?.is1to1() || !call) {
return;
}

const selfParticipant = call.getSelfParticipant();

const nextOtherParticipant = members.find(
participant => !matchQualifiedIds(participant.userId, selfParticipant.user.qualifiedId),
);

if (!nextOtherParticipant) {
return;
}

const currentOtherParticipant = call
.participants()
.find(participant => matchQualifiedIds(nextOtherParticipant.userId, participant.user.qualifiedId));

if (!currentOtherParticipant) {
return;
}

const isCurrentlyEstablished = currentOtherParticipant.isAudioEstablished();
const {aestab: newEstablishedStatus} = nextOtherParticipant;

if (isCurrentlyEstablished && newEstablishedStatus === AUDIO_STATE.CONNECTING) {
void this.leave1on1MLSConference(conversationId);
}
};

private readonly requestClients = async (wUser: number, convId: SerializedConversationId, __: number) => {
const call = this.findCall(this.parseQualifiedId(convId));
if (!call) {
Expand All @@ -1795,7 +1880,7 @@ export class CallingRepository {

const {conversation} = call;

if (isGroupMLSConversation(conversation)) {
if (conversation && this.isMLSConference(conversation)) {
const subconversationEpochInfo = await this.subconversationService.getSubconversationEpochInfo(
conversation.qualifiedId,
conversation.groupId,
Expand Down
16 changes: 7 additions & 9 deletions src/script/conversation/ConversationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(mls1to1ConversationResponse);
.mockResolvedValueOnce({conversation: mls1to1ConversationResponse});

jest
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
Expand Down Expand Up @@ -694,8 +694,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(mls1to1ConversationResponse);

.mockResolvedValueOnce({conversation: mls1to1ConversationResponse});
jest
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
.mockResolvedValueOnce(false);
Expand Down Expand Up @@ -779,8 +778,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(mls1to1ConversationResponse);

.mockResolvedValueOnce({conversation: mls1to1ConversationResponse});
jest
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
.mockResolvedValueOnce(false);
Expand Down Expand Up @@ -846,7 +844,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(mls1to1ConversationResponse);
.mockResolvedValueOnce({conversation: mls1to1ConversationResponse});

jest
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
Expand All @@ -869,7 +867,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(establishedMls1to1ConversationResponse);
.mockResolvedValueOnce({conversation: establishedMls1to1ConversationResponse});
jest
.spyOn(container.resolve(Core).service!.conversation, 'establishMLS1to1Conversation')
.mockResolvedValueOnce(establishedMls1to1ConversationResponse);
Expand Down Expand Up @@ -918,7 +916,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(establishedMls1to1ConversationResponse);
.mockResolvedValueOnce({conversation: establishedMls1to1ConversationResponse});
jest
.spyOn(container.resolve(Core).service!.conversation, 'establishMLS1to1Conversation')
.mockResolvedValueOnce(establishedMls1to1ConversationResponse);
Expand Down Expand Up @@ -1145,7 +1143,7 @@ describe('ConversationRepository', () => {

jest
.spyOn(conversationRepository['conversationService'], 'getMLS1to1Conversation')
.mockResolvedValueOnce(mls1to1ConversationResponse);
.mockResolvedValueOnce({conversation: mls1to1ConversationResponse});

jest
.spyOn(conversationRepository['conversationService'], 'isMLSGroupEstablishedLocally')
Expand Down
2 changes: 1 addition & 1 deletion src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1589,7 +1589,7 @@ export class ConversationRepository {
* @returns MLS conversation entity
*/
private readonly fetchMLS1to1Conversation = async (otherUserId: QualifiedId): Promise<MLSConversation> => {
const remoteConversation = await this.conversationService.getMLS1to1Conversation(otherUserId);
const {conversation: remoteConversation} = await this.conversationService.getMLS1to1Conversation(otherUserId);
const [conversation] = this.mapConversations([remoteConversation]);

if (!isMLSConversation(conversation)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ describe('FeatureConfigChangeNotifier', () => {
config: {enforcedTimeoutSeconds: 0},
},
[FEATURE_KEY.CONFERENCE_CALLING]: {
config: {useSFTForOneToOneCalls: false},
status: FeatureStatus.DISABLED,
config: {},
},
[FEATURE_KEY.CONVERSATION_GUEST_LINKS]: {
status: FeatureStatus.DISABLED,
Expand Down
4 changes: 1 addition & 3 deletions src/script/team/TeamService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,8 @@ export class TeamService {
status: FeatureStatus.DISABLED,
},
[FEATURE_KEY.CONFERENCE_CALLING]: {
config: {useSFTForOneToOneCalls: false},
status: FeatureStatus.ENABLED,
config: {
useSFTForOneToOneCalls: false,
},
},
[FEATURE_KEY.DIGITAL_SIGNATURES]: {
status: FeatureStatus.ENABLED,
Expand Down
Loading

0 comments on commit d89b2a9

Please sign in to comment.