Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Make CallingRepository a little more stateless #14553

Merged
merged 7 commits into from
Jan 26, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 4 additions & 68 deletions src/script/calling/CallingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,6 @@ export class CallingRepository {
const convId = this.serializeQualifiedId(conversationId);
this.logger.log(`Starting a call of type "${callType}" in conversation ID "${convId}"...`);
try {
await this.checkConcurrentJoinedCall(conversationId, CALL_STATE.OUTGOING);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the responsibility of the CallingRepo to check if there is a call ongoing. This has been moved to the view layer (see CallingViewModel)

conversationType =
conversationType === CONV_TYPE.GROUP && this.supportsConferenceCalling
? CONV_TYPE.CONFERENCE
Expand Down Expand Up @@ -813,7 +812,6 @@ export class CallingRepository {
async answerCall(call: Call, callType?: CALL_TYPE): Promise<void> {
try {
callType ??= call.getSelfParticipant().sharesCamera() ? call.initialType : CALL_TYPE.NORMAL;
await this.checkConcurrentJoinedCall(call.conversationId, CALL_STATE.INCOMING);

const isVideoCall = callType === CALL_TYPE.VIDEO;
if (!isVideoCall) {
Expand Down Expand Up @@ -849,18 +847,14 @@ export class CallingRepository {
this.wCall?.reject(this.wUser, this.serializeQualifiedId(conversationId));
}

changeCallPage(newPage: number, call: Call): void {
changeCallPage(call: Call, newPage: number): void {
call.currentPage(newPage);
if (!this.callState.isSpeakersViewActive()) {
this.requestCurrentPageVideoStreams();
this.requestCurrentPageVideoStreams(call);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now inject the call that is concerned with the paging change in order for the CallingRepo not to have to check the state

}
}

requestCurrentPageVideoStreams(): void {
const call = this.callState.joinedCall();
if (!call) {
return;
}
requestCurrentPageVideoStreams(call: Call): void {
const currentPageParticipants = call.pages()[call.currentPage()];
this.requestVideoStreams(call.conversationId, currentPageParticipants);
}
Expand Down Expand Up @@ -1436,7 +1430,7 @@ export class CallingRepository {
}

call.updatePages();
this.changeCallPage(call.currentPage(), call);
this.changeCallPage(call, call.currentPage());
}

private readonly handleCallParticipantChanges = (convId: SerializedConversationId, membersJson: string) => {
Expand Down Expand Up @@ -1715,64 +1709,6 @@ export class CallingRepository {
return this.apiClient.api.account.getCallConfig(limit);
}

private checkConcurrentJoinedCall(conversationId: QualifiedId, newCallState: CALL_STATE): Promise<void> {
const idleCallStates = [CALL_STATE.INCOMING, CALL_STATE.NONE, CALL_STATE.UNKNOWN];
const activeCall = this.callState
.calls()
.find(call => !matchQualifiedIds(call.conversationId, conversationId) && !idleCallStates.includes(call.state()));
if (!activeCall) {
return Promise.resolve();
}

let actionString: string;
let messageString: string;
let titleString: string;

switch (newCallState) {
case CALL_STATE.INCOMING: {
actionString = t('modalCallSecondIncomingAction');
messageString = t('modalCallSecondIncomingMessage');
titleString = t('modalCallSecondIncomingHeadline');
break;
}

case CALL_STATE.OUTGOING: {
actionString = t('modalCallSecondOutgoingAction');
messageString = t('modalCallSecondOutgoingMessage');
titleString = t('modalCallSecondOutgoingHeadline');
break;
}

default: {
return Promise.reject(`Tried to join second call in unexpected state '${newCallState}'`);
}
}

return new Promise((resolve, reject) => {
PrimaryModal.show(PrimaryModal.type.CONFIRM, {
primaryAction: {
action: () => {
if (activeCall.state() === CALL_STATE.INCOMING) {
this.rejectCall(activeCall.conversationId);
} else {
this.leaveCall(activeCall.conversationId, LEAVE_CALL_REASON.MANUAL_LEAVE_TO_JOIN_ANOTHER_CALL);
}
window.setTimeout(resolve, 1000);
},
text: actionString,
},
secondaryAction: {
action: reject,
},
text: {
message: messageString,
title: titleString,
},
});
this.logger.warn(`Tried to join a second call while calling in conversation '${activeCall.conversationId}'.`);
});
}

private showNoAudioInputModal(): void {
const modalOptions = {
primaryAction: {
Expand Down
6 changes: 3 additions & 3 deletions src/script/components/calling/CallingOverlayContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const CallingContainer: React.FC<CallingContainerProps> = ({
};

const changePage = (newPage: number, call: Call) => {
callingRepository.changeCallPage(newPage, call);
callingRepository.changeCallPage(call, newPage);
};

const leave = (call: Call) => {
Expand All @@ -106,8 +106,8 @@ const CallingContainer: React.FC<CallingContainerProps> = ({

const setActiveCallViewTab = (tab: string) => {
callState.activeCallViewTab(tab);
if (tab === CallViewTab.ALL) {
callingRepository.requestCurrentPageVideoStreams();
if (tab === CallViewTab.ALL && joinedCall) {
callingRepository.requestCurrentPageVideoStreams(joinedCall);
}
};

Expand Down
90 changes: 81 additions & 9 deletions src/script/view_model/CallingViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {Availability} from '@wireapp/protocol-messaging';
import {ButtonGroupTab} from 'Components/calling/ButtonGroup';
import 'Components/calling/ChooseScreen';
import {replaceLink, t} from 'Util/LocalizerUtil';
import {matchQualifiedIds} from 'Util/QualifiedId';
import {safeWindowOpen} from 'Util/SanitizationUtil';

import type {AudioRepository} from '../audio/AudioRepository';
Expand Down Expand Up @@ -136,14 +137,22 @@ export class CallingViewModel {
});
};

const startCall = (conversationEntity: Conversation, callType: CALL_TYPE): void => {
const convType = conversationEntity.isGroup() ? CONV_TYPE.GROUP : CONV_TYPE.ONEONONE;
this.callingRepository.startCall(conversationEntity.qualifiedId, convType, callType).then(call => {
if (!call) {
return;
}
ring(call);
const startCall = async (conversation: Conversation, callType: CALL_TYPE) => {
const convType = conversation.isGroup() ? CONV_TYPE.GROUP : CONV_TYPE.ONEONONE;
const canStart = await this.canInitiateCall(conversation.qualifiedId, {
action: t('modalCallSecondOutgoingAction'),
message: t('modalCallSecondOutgoingMessage'),
title: t('modalCallSecondOutgoingHeadline'),
});

if (!canStart) {
return;
}
const call = await this.callingRepository.startCall(conversation.qualifiedId, convType, callType);
if (!call) {
return;
}
ring(call);
};

const hasSoundlessCallsEnabled = (): boolean => {
Expand All @@ -162,7 +171,7 @@ export class CallingViewModel {
});

this.callActions = {
answer: (call: Call) => {
answer: async (call: Call) => {
if (call.conversationType === CONV_TYPE.CONFERENCE && !this.callingRepository.supportsConferenceCalling) {
PrimaryModal.show(PrimaryModal.type.ACKNOWLEDGE, {
primaryAction: {
Expand All @@ -178,11 +187,20 @@ export class CallingViewModel {
},
});
} else {
const canAnwer = await this.canInitiateCall(call.conversationId, {
action: t('modalCallSecondIncomingAction'),
message: t('modalCallSecondIncomingMessage'),
title: t('modalCallSecondIncomingHeadline'),
});
if (!canAnwer) {
return;
}

this.callingRepository.answerCall(call);
}
},
changePage: (newPage, call) => {
this.callingRepository.changeCallPage(newPage, call);
this.callingRepository.changeCallPage(call, newPage);
},
leave: (call: Call) => {
this.callingRepository.leaveCall(call.conversationId, LEAVE_CALL_REASON.MANUAL_LEAVE_BY_UI_CLICK);
Expand Down Expand Up @@ -252,6 +270,60 @@ export class CallingViewModel {
};
}

/**
* Will reject or leave the call depending on the state of the call.
* @param activeCall - the call to gracefully tear down
*/
private async gracefullyTeardownCall(activeCall: Call): Promise<void> {
if (activeCall.state() === CALL_STATE.INCOMING) {
this.callingRepository.rejectCall(activeCall.conversationId);
} else {
this.callingRepository.leaveCall(activeCall.conversationId, LEAVE_CALL_REASON.MANUAL_LEAVE_TO_JOIN_ANOTHER_CALL);
}
// We want to wait a bit to be sure the call have been tear down properly
await new Promise(resolve => setTimeout(resolve, 1000));
}

/**
* Will make sure everything is ready for a call to start/be joined in the given conversation.
* If there is another ongoing call, the user will be asked to first leave that other call before starting a new call.
*
* @param conversationId - the conversation in which the call should be started/joined
* @param warningStrings - the strings to display in case there is already an active call
* @returns true if the call can be started, false otherwise
*/
private canInitiateCall(
conversationId: QualifiedId,
warningStrings: {action: string; message: string; title: string},
): Promise<boolean> {
const idleCallStates = [CALL_STATE.INCOMING, CALL_STATE.NONE, CALL_STATE.UNKNOWN];
const otherActiveCall = this.callState
.calls()
.find(call => !matchQualifiedIds(call.conversationId, conversationId) && !idleCallStates.includes(call.state()));
if (!otherActiveCall) {
return Promise.resolve(true);
}

return new Promise(resolve => {
PrimaryModal.show(PrimaryModal.type.CONFIRM, {
primaryAction: {
action: async () => {
await this.gracefullyTeardownCall(otherActiveCall);
resolve(true);
},
text: warningStrings.action,
},
secondaryAction: {
action: () => resolve(false),
},
text: {
message: warningStrings.message,
title: warningStrings.title,
},
});
});
}

private showRestrictedConferenceCallingModal() {
if (this.selfUser().inTeam()) {
if (this.selfUser().teamRole() === ROLE.OWNER) {
Expand Down