From 2d2e0e0133a8fc464b92f49446deeeb8caf6f22c Mon Sep 17 00:00:00 2001 From: streamer45 Date: Thu, 2 Jun 2022 12:17:35 +0200 Subject: [PATCH 1/2] Implement MaxCallParticipants config setting --- .../components/call_message/call_message.tsx | 12 ++++---- .../calls/components/call_message/index.ts | 4 +-- .../components/channel_info/start_call.tsx | 6 ++-- .../calls/components/join_call/index.ts | 4 +-- .../components/join_call/join_call.test.js | 2 +- .../calls/components/join_call/join_call.tsx | 8 +++--- app/products/calls/store/selectors/calls.ts | 28 ++++++------------- app/products/calls/store/types/calls.ts | 4 +-- 8 files changed, 28 insertions(+), 40 deletions(-) diff --git a/app/products/calls/components/call_message/call_message.tsx b/app/products/calls/components/call_message/call_message.tsx index 54426cfd421..4be378fdd2e 100644 --- a/app/products/calls/components/call_message/call_message.tsx +++ b/app/products/calls/components/call_message/call_message.tsx @@ -32,7 +32,7 @@ type CallMessageProps = { currentChannelName: string; callChannelName: string; intl: typeof IntlShape; - isCloudLimitRestricted: boolean; + isLimitRestricted: boolean; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { @@ -121,11 +121,11 @@ const CallMessage = ({ currentChannelName, callChannelName, intl, - isCloudLimitRestricted, + isLimitRestricted, }: CallMessageProps) => { const style = getStyleSheet(theme); const joinHandler = () => { - if (alreadyInTheCall || isCloudLimitRestricted) { + if (alreadyInTheCall || isLimitRestricted) { return; } leaveAndJoinWithAlert(intl, post.channel_id, callChannelName, currentChannelName, confirmToJoin, actions.joinCall); @@ -187,15 +187,15 @@ const CallMessage = ({ - + {joinCallButtonText} diff --git a/app/products/calls/components/call_message/index.ts b/app/products/calls/components/call_message/index.ts index e82eca7742e..647f302e555 100644 --- a/app/products/calls/components/call_message/index.ts +++ b/app/products/calls/components/call_message/index.ts @@ -11,7 +11,7 @@ import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; import {getUser, getCurrentUser} from '@mm-redux/selectors/entities/users'; import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils'; import {joinCall} from '@mmproducts/calls/store/actions/calls'; -import {getCalls, getCurrentCall, isCloudLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; +import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; import CallMessage from './call_message'; @@ -41,7 +41,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) { userTimezone: enableTimezone ? getUserCurrentTimezone(currentUser.timezone) : undefined, currentChannelName: getChannel(state, post.channel_id)?.display_name, callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '', - isCloudLimitRestricted: isCloudLimitRestricted(state, post.channel_id), + isLimitRestricted: isLimitRestricted(state, post.channel_id), }; } diff --git a/app/products/calls/components/channel_info/start_call.tsx b/app/products/calls/components/channel_info/start_call.tsx index e1091c89c3c..ab7ed9f4392 100644 --- a/app/products/calls/components/channel_info/start_call.tsx +++ b/app/products/calls/components/channel_info/start_call.tsx @@ -10,7 +10,7 @@ import {GlobalState} from '@mm-redux/types/store'; import {Theme} from '@mm-redux/types/theme'; import leaveAndJoinWithAlert from '@mmproducts/calls/components/leave_and_join_alert'; import {useTryCallsFunction} from '@mmproducts/calls/hooks'; -import {getCalls, getCurrentCall, isCloudLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; +import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; import ChannelInfoRow from '@screens/channel_info/channel_info_row'; import Separator from '@screens/channel_info/separator'; import {preventDoubleTap} from '@utils/tap'; @@ -28,7 +28,7 @@ const StartCall = ({testID, theme, intl, joinCall}: Props) => { const currentCall = useSelector(getCurrentCall); const currentCallChannelId = currentCall?.channelId || ''; const callChannelName = useSelector((state: GlobalState) => getChannel(state, currentCallChannelId)?.display_name) || ''; - const cloudLimitRestricted = useSelector(isCloudLimitRestricted); + const limitRestricted = useSelector(isLimitRestricted); const confirmToJoin = Boolean(currentCall && currentCall.channelId !== currentChannel.id); const alreadyInTheCall = Boolean(currentCall && call && currentCall.channelId === call.channelId); @@ -40,7 +40,7 @@ const StartCall = ({testID, theme, intl, joinCall}: Props) => { const [tryLeaveAndJoin, msgPostfix] = useTryCallsFunction(leaveAndJoin); const handleStartCall = useCallback(preventDoubleTap(tryLeaveAndJoin), [tryLeaveAndJoin]); - if (alreadyInTheCall || cloudLimitRestricted) { + if (alreadyInTheCall || limitRestricted) { return null; } diff --git a/app/products/calls/components/join_call/index.ts b/app/products/calls/components/join_call/index.ts index 10c8d8999b0..e7275e52f13 100644 --- a/app/products/calls/components/join_call/index.ts +++ b/app/products/calls/components/join_call/index.ts @@ -6,7 +6,7 @@ import {bindActionCreators, Dispatch} from 'redux'; import {getChannel, getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {joinCall} from '@mmproducts/calls/store/actions/calls'; -import {getCalls, getCurrentCall, isCloudLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; +import {getCalls, getCurrentCall, isLimitRestricted} from '@mmproducts/calls/store/selectors/calls'; import JoinCall from './join_call'; @@ -23,7 +23,7 @@ function mapStateToProps(state: GlobalState) { alreadyInTheCall: Boolean(currentCall && call && currentCall.channelId === call.channelId), currentChannelName: getChannel(state, currentChannelId)?.display_name, callChannelName: currentCall ? getChannel(state, currentCall.channelId)?.display_name : '', - isCloudLimitRestricted: isCloudLimitRestricted(state), + isLimitRestricted: isLimitRestricted(state), }; } diff --git a/app/products/calls/components/join_call/join_call.test.js b/app/products/calls/components/join_call/join_call.test.js index 535175c8d17..08821d28b30 100644 --- a/app/products/calls/components/join_call/join_call.test.js +++ b/app/products/calls/components/join_call/join_call.test.js @@ -38,7 +38,7 @@ describe('JoinCall', () => { alreadyInTheCall: false, currentChannelName: 'Current Channel', callChannelName: 'Call Channel', - isCloudLimitRestricted: false, + isLimitRestricted: false, }; test('should match snapshot', () => { diff --git a/app/products/calls/components/join_call/join_call.tsx b/app/products/calls/components/join_call/join_call.tsx index 102c2a19137..2068078d75a 100644 --- a/app/products/calls/components/join_call/join_call.tsx +++ b/app/products/calls/components/join_call/join_call.tsx @@ -26,7 +26,7 @@ type Props = { alreadyInTheCall: boolean; currentChannelName: string; callChannelName: string; - isCloudLimitRestricted: boolean; + isLimitRestricted: boolean; intl: typeof IntlShape; } @@ -96,7 +96,7 @@ const JoinCall = (props: Props) => { }, [props.call, props.alreadyInTheCall]); const joinHandler = () => { - if (props.isCloudLimitRestricted) { + if (props.isLimitRestricted) { return; } leaveAndJoinWithAlert(props.intl, props.call.channelId, props.callChannelName, props.currentChannelName, props.confirmToJoin, props.actions.joinCall); @@ -114,7 +114,7 @@ const JoinCall = (props: Props) => { return ( { style={style.joinCallIcon} /> {'Join Call'} - {props.isCloudLimitRestricted ? + {props.isLimitRestricted ? {'Participant limit reached'} : diff --git a/app/products/calls/store/selectors/calls.ts b/app/products/calls/store/selectors/calls.ts index 9c5828b6b17..d456ee34ca2 100644 --- a/app/products/calls/store/selectors/calls.ts +++ b/app/products/calls/store/selectors/calls.ts @@ -92,37 +92,25 @@ const isCloud: (state: GlobalState) => boolean = createSelector( (license) => license?.Cloud === 'true', ); -// NOTE: Calls v0.5.3 will not return sku_short_name in config, so this will fail -const isCloudProfessionalOrEnterprise: (state: GlobalState) => boolean = createSelector( +export const isLimitRestricted: (state: GlobalState, channelId?: string) => boolean = createSelector( isCloud, - getLicense, - getConfig, - (cloud, license, config) => { - return cloud && (config.sku_short_name === 'professional' || config.sku_short_name === 'enterprise'); - }, -); - -export const isCloudLimitRestricted: (state: GlobalState, channelId?: string) => boolean = createSelector( - isCloud, - isCloudProfessionalOrEnterprise, (state: GlobalState, channelId: string) => (channelId ? getCalls(state)[channelId] : getCallInCurrentChannel(state)), getConfig, - (cloud, isCloudPaid, call, config) => { - if (!call || !cloud) { + (cloud, call, config) => { + if (!call) { return false; } + const numParticipants = Object.keys(call.participants || {}).length; + // TODO: The next block is used for case when cloud server is using Calls v0.5.3. This can be removed // when v0.5.4 is prepackaged in cloud. Then replace the max in the return statement with - // config.cloud_max_participants, and replace cloudPaid with isCloudPaid - let max = config.cloud_max_participants; - let cloudPaid = isCloudPaid; + // config.MaxCallParticipants. + let max = config.MaxCallParticipants; if (cloud && !max) { - // We're not sure if we're in cloud paid because this could be v0.5.3, so assume we are for now (the server will prevent calls) - cloudPaid = true; max = Calls.DefaultCloudMaxParticipants; } - return cloudPaid && Object.keys(call.participants || {}).length >= max; + return max !== 0 && numParticipants >= max; }, ); diff --git a/app/products/calls/store/types/calls.ts b/app/products/calls/store/types/calls.ts index fb3b85f4214..83dcdfdcbdd 100644 --- a/app/products/calls/store/types/calls.ts +++ b/app/products/calls/store/types/calls.ts @@ -62,8 +62,8 @@ export type ServerConfig = { ICEServers: string[]; AllowEnableCalls: boolean; DefaultEnabled: boolean; + MaxCallParticipants: number; sku_short_name: string; - cloud_max_participants: number; last_retrieved_at: number; } @@ -71,7 +71,7 @@ export const DefaultServerConfig = { ICEServers: [], AllowEnableCalls: false, DefaultEnabled: false, + MaxCallParticipants: 0, sku_short_name: '', - cloud_max_participants: 0, last_retrieved_at: 0, } as ServerConfig; From ea5d737e46e970d5cd8ae3f0be5b2ba048984211 Mon Sep 17 00:00:00 2001 From: streamer45 Date: Thu, 2 Jun 2022 13:41:11 +0200 Subject: [PATCH 2/2] Add test --- .../calls/store/selectors/calls.test.js | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/app/products/calls/store/selectors/calls.test.js b/app/products/calls/store/selectors/calls.test.js index a1f5271af83..9b98d68e87e 100644 --- a/app/products/calls/store/selectors/calls.test.js +++ b/app/products/calls/store/selectors/calls.test.js @@ -4,11 +4,12 @@ import assert from 'assert'; import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze'; +import {DefaultServerConfig} from '@mmproducts/calls/store/types/calls'; import * as Selectors from './calls'; describe('Selectors.Calls', () => { - const call1 = {id: 'call1'}; + const call1 = {id: 'call1', participants: [{id: 'me'}]}; const call2 = {id: 'call2'}; const testState = deepFreezeAndThrowOnMutation({ entities: { @@ -20,6 +21,10 @@ describe('Selectors.Calls', () => { joined: 'call1', enabled: {'channel-1': true, 'channel-2': false}, screenShareURL: 'screenshare-url', + config: DefaultServerConfig, + }, + general: { + license: {}, }, }, }); @@ -77,4 +82,119 @@ describe('Selectors.Calls', () => { it('getScreenShareURL', () => { assert.equal(Selectors.getScreenShareURL(testState), 'screenshare-url'); }); + + it('isLimitRestricted', () => { + // Default, no limit + assert.equal(Selectors.isLimitRestricted(testState, 'call1'), false); + + let newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + config: { + ...testState.entities.calls.config, + MaxCallParticipants: 1, + }, + }, + }, + }; + + // Limit to 1 and one participant already in call. + assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true); + + // Limit to 1 but no call ongoing. + assert.equal(Selectors.isLimitRestricted(newState), false); + + newState = { + ...testState, + entities: { + ...testState.entities, + general: { + license: {Cloud: 'true'}, + }, + }, + }; + + // On cloud, no limit. + assert.equal(Selectors.isLimitRestricted(newState, 'call1'), false); + + newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + config: { + ...testState.entities.calls.config, + MaxCallParticipants: 1, + }, + }, + general: { + license: {Cloud: 'true'}, + }, + }, + }; + + // On cloud, with limit. + assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true); + + const call = {id: 'call1', + participants: [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + ]}; + newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + calls: {call1: call}, + }, + general: { + license: {Cloud: 'true'}, + }, + }, + }; + delete newState.entities.calls.config.MaxCallParticipants; + + // On cloud, MaxCallParticipants missing, default should be used. + assert.equal(Selectors.isLimitRestricted(newState, 'call1'), false); + + const newCall = {id: 'call1', + participants: [ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + ]}; + newState = { + ...testState, + entities: { + ...testState.entities, + calls: { + ...testState.entities.calls, + calls: {call1: newCall}, + }, + general: { + license: {Cloud: 'true'}, + }, + }, + }; + delete newState.entities.calls.config.MaxCallParticipants; + + // On cloud, MaxCallParticipants missing, default should be used. + assert.equal(Selectors.isLimitRestricted(newState, 'call1'), true); + }); });