diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5ed62affbc..8859f175a4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,14 @@
+
+
+
+
+
+
diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx
index 7b908ca7f9..970608cece 100644
--- a/app/components/post_list/post/post.tsx
+++ b/app/components/post_list/post/post.tsx
@@ -277,6 +277,7 @@ const Post = ({
} else if (isCallsPost && !hasBeenDeleted) {
body = (
);
diff --git a/app/constants/index.ts b/app/constants/index.ts
index 924a5c4fdd..4305c740d5 100644
--- a/app/constants/index.ts
+++ b/app/constants/index.ts
@@ -17,6 +17,7 @@ import Files from './files';
import General from './general';
import Integrations from './integrations';
import Launch from './launch';
+import License from './license';
import List from './list';
import Navigation from './navigation';
import Network from './network';
@@ -52,6 +53,7 @@ export {
General,
Integrations,
Launch,
+ License,
List,
Navigation,
Network,
diff --git a/app/constants/license.ts b/app/constants/license.ts
new file mode 100644
index 0000000000..7016c3d57a
--- /dev/null
+++ b/app/constants/license.ts
@@ -0,0 +1,12 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export default {
+ SKU_SHORT_NAME: {
+ E10: 'E10',
+ E20: 'E20',
+ Starter: 'starter',
+ Professional: 'professional',
+ Enterprise: 'enterprise',
+ },
+};
diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts
index 82712d0b95..926424da24 100644
--- a/app/products/calls/actions/calls.ts
+++ b/app/products/calls/actions/calls.ts
@@ -71,7 +71,7 @@ export const loadConfig = async (serverUrl: string, force = false) => {
return {error};
}
- const nextConfig = {...config, ...data, last_retrieved_at: now};
+ const nextConfig = {...data, last_retrieved_at: now};
setConfig(serverUrl, nextConfig);
return {data: nextConfig};
};
@@ -189,7 +189,10 @@ export const checkIsCallsPluginEnabled = async (serverUrl: string) => {
}
const enabled = data.findIndex((m) => m.id === Calls.PluginId) !== -1;
- setPluginEnabled(serverUrl, enabled);
+ const curEnabled = getCallsConfig(serverUrl).pluginEnabled;
+ if (enabled !== curEnabled) {
+ setPluginEnabled(serverUrl, enabled);
+ }
return {data: enabled};
};
diff --git a/app/products/calls/alerts.ts b/app/products/calls/alerts.ts
new file mode 100644
index 0000000000..5c9ed9dfac
--- /dev/null
+++ b/app/products/calls/alerts.ts
@@ -0,0 +1,32 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Alert} from 'react-native';
+
+import type {IntlShape} from 'react-intl';
+
+export const showLimitRestrictedAlert = (maxParticipants: number, intl: IntlShape) => {
+ const title = intl.formatMessage({
+ id: 'mobile.calls_participant_limit_title',
+ defaultMessage: 'Participant limit reached',
+ });
+ const message = intl.formatMessage({
+ id: 'mobile.calls_limit_msg',
+ defaultMessage: 'The maximum number of participants per call is {maxParticipants}. Contact your System Admin to increase the limit.',
+ }, {maxParticipants});
+ const ok = intl.formatMessage({
+ id: 'mobile.calls_ok',
+ defaultMessage: 'Okay',
+ });
+
+ Alert.alert(
+ title,
+ message,
+ [
+ {
+ text: ok,
+ style: 'cancel',
+ },
+ ],
+ );
+};
diff --git a/app/products/calls/components/calls_custom_message/calls_custom_message.tsx b/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
index 70916472d0..e9bd83ba71 100644
--- a/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
+++ b/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
@@ -6,6 +6,7 @@ import React from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
+import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
@@ -16,6 +17,7 @@ import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername, getUserTimezone} from '@utils/user';
+import type {LimitRestrictedInfo} from '@calls/observers';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
@@ -28,6 +30,7 @@ type Props = {
currentCallChannelId?: string;
leaveChannelName?: string;
joinChannelName?: string;
+ limitRestrictedInfo?: LimitRestrictedInfo;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -62,10 +65,16 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
joinCallButtonText: {
color: 'white',
},
+ joinCallButtonTextRestricted: {
+ color: changeOpacity(theme.centerChannelColor, 0.32),
+ },
joinCallButtonIcon: {
color: 'white',
marginRight: 5,
},
+ joinCallButtonIconRestricted: {
+ color: changeOpacity(theme.centerChannelColor, 0.32),
+ },
startedText: {
color: theme.centerChannelColor,
fontWeight: 'bold',
@@ -78,6 +87,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
alignItems: 'center',
alignContent: 'center',
},
+ joinCallButtonRestricted: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
+ },
timeText: {
color: theme.centerChannelColor,
},
@@ -96,7 +108,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
export const CallsCustomMessage = ({
post, currentUser, author, isMilitaryTime, teammateNameDisplay,
- currentCallChannelId, leaveChannelName, joinChannelName,
+ currentCallChannelId, leaveChannelName, joinChannelName, limitRestrictedInfo,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
@@ -106,12 +118,18 @@ export const CallsCustomMessage = ({
const confirmToJoin = Boolean(currentCallChannelId && currentCallChannelId !== post.channelId);
const alreadyInTheCall = Boolean(currentCallChannelId && currentCallChannelId === post.channelId);
+ const isLimitRestricted = Boolean(limitRestrictedInfo?.limitRestricted);
const joinHandler = () => {
if (alreadyInTheCall) {
return;
}
+ if (isLimitRestricted) {
+ showLimitRestrictedAlert(limitRestrictedInfo!.maxParticipants, intl);
+ return;
+ }
+
leaveAndJoinWithAlert(intl, serverUrl, post.channelId, leaveChannelName || '', joinChannelName || '', confirmToJoin, false);
};
@@ -157,6 +175,7 @@ export const CallsCustomMessage = ({
);
}
+ const joinTextStyle = [style.joinCallButtonText, isLimitRestricted && style.joinCallButtonTextRestricted];
return (
{
alreadyInTheCall &&
}
{
@@ -200,7 +219,7 @@ export const CallsCustomMessage = ({
}
diff --git a/app/products/calls/components/calls_custom_message/index.ts b/app/products/calls/components/calls_custom_message/index.ts
index 881c696094..4ab7ecef87 100644
--- a/app/products/calls/components/calls_custom_message/index.ts
+++ b/app/products/calls/components/calls_custom_message/index.ts
@@ -7,6 +7,7 @@ import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {CallsCustomMessage} from '@calls/components/calls_custom_message/calls_custom_message';
+import {observeIsCallLimitRestricted} from '@calls/observers';
import {observeCurrentCall} from '@calls/state';
import {Preferences} from '@constants';
import DatabaseManager from '@database/manager';
@@ -18,7 +19,12 @@ import {observeCurrentUser, observeTeammateNameDisplay, observeUser} from '@quer
import type {WithDatabaseArgs} from '@typings/database/database';
import type PostModel from '@typings/database/models/servers/post';
-const enhanced = withObservables(['post'], ({post, database}: { post: PostModel } & WithDatabaseArgs) => {
+type OwnProps = {
+ serverUrl: string;
+ post: PostModel;
+}
+
+const enhanced = withObservables(['post'], ({serverUrl, post, database}: OwnProps & WithDatabaseArgs) => {
const currentUser = observeCurrentUser(database);
const author = observeUser(database, post.userId);
const isMilitaryTime = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observeWithColumns(['value']).pipe(
@@ -63,6 +69,7 @@ const enhanced = withObservables(['post'], ({post, database}: { post: PostModel
currentCallChannelId,
leaveChannelName,
joinChannelName,
+ limitRestrictedInfo: observeIsCallLimitRestricted(serverUrl, post.channelId),
};
});
diff --git a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
index 4e40814537..9d0e453a0d 100644
--- a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
+++ b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
@@ -5,11 +5,14 @@ import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {leaveCall} from '@calls/actions';
+import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import {useTryCallsFunction} from '@calls/hooks';
import OptionBox from '@components/option_box';
import {preventDoubleTap} from '@utils/tap';
+import type {LimitRestrictedInfo} from '@calls/observers';
+
export interface Props {
serverUrl: string;
displayName: string;
@@ -19,6 +22,7 @@ export interface Props {
alreadyInCall: boolean;
currentCallChannelName: string;
dismissChannelInfo: () => void;
+ limitRestrictedInfo: LimitRestrictedInfo;
}
const ChannelInfoStartButton = ({
@@ -30,18 +34,23 @@ const ChannelInfoStartButton = ({
alreadyInCall,
currentCallChannelName,
dismissChannelInfo,
+ limitRestrictedInfo,
}: Props) => {
const intl = useIntl();
+ const isLimitRestricted = limitRestrictedInfo.limitRestricted;
const toggleJoinLeave = useCallback(() => {
if (alreadyInCall) {
leaveCall();
+ } else if (isLimitRestricted) {
+ showLimitRestrictedAlert(limitRestrictedInfo.maxParticipants, intl);
} else {
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, confirmToJoin, !isACallInCurrentChannel);
}
dismissChannelInfo();
- }, [alreadyInCall, dismissChannelInfo, intl, serverUrl, channelId, currentCallChannelName, displayName, confirmToJoin, isACallInCurrentChannel]);
+ }, [isLimitRestricted, alreadyInCall, dismissChannelInfo, intl, serverUrl, channelId, currentCallChannelName, displayName, confirmToJoin, isACallInCurrentChannel]);
+
const [tryJoin, msgPostfix] = useTryCallsFunction(toggleJoinLeave);
const joinText = intl.formatMessage({id: 'mobile.calls_join_call', defaultMessage: 'Join call'});
diff --git a/app/products/calls/components/channel_info_start/index.ts b/app/products/calls/components/channel_info_start/index.ts
index cb0c142f66..79e8faf83a 100644
--- a/app/products/calls/components/channel_info_start/index.ts
+++ b/app/products/calls/components/channel_info_start/index.ts
@@ -7,6 +7,7 @@ import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import ChannelInfoStartButton from '@calls/components/channel_info_start/channel_info_start_button';
+import {observeIsCallLimitRestricted} from '@calls/observers';
import {observeChannelsWithCalls, observeCurrentCall} from '@calls/state';
import DatabaseManager from '@database/manager';
import {observeChannel} from '@queries/servers/channel';
@@ -52,6 +53,7 @@ const enhanced = withObservables([], ({serverUrl, channelId, database}: EnhanceP
alreadyInCall,
currentCall,
currentCallChannelName,
+ limitRestrictedInfo: observeIsCallLimitRestricted(serverUrl, channelId),
};
});
diff --git a/app/products/calls/components/join_call_banner/index.ts b/app/products/calls/components/join_call_banner/index.ts
index 5022b10fd9..4dba91a15d 100644
--- a/app/products/calls/components/join_call_banner/index.ts
+++ b/app/products/calls/components/join_call_banner/index.ts
@@ -7,6 +7,7 @@ import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import JoinCallBanner from '@calls/components/join_call_banner/join_call_banner';
+import {observeIsCallLimitRestricted} from '@calls/observers';
import {observeCallsState, observeCurrentCall} from '@calls/state';
import {idsAreEqual} from '@calls/utils';
import {observeChannel} from '@queries/servers/channel';
@@ -60,6 +61,7 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
inACall,
currentCallChannelName,
channelCallStartTime,
+ limitRestrictedInfo: observeIsCallLimitRestricted(serverUrl, channelId),
};
});
diff --git a/app/products/calls/components/join_call_banner/join_call_banner.tsx b/app/products/calls/components/join_call_banner/join_call_banner.tsx
index ae17f90346..2806f3fc09 100644
--- a/app/products/calls/components/join_call_banner/join_call_banner.tsx
+++ b/app/products/calls/components/join_call_banner/join_call_banner.tsx
@@ -3,8 +3,9 @@
import React from 'react';
import {useIntl} from 'react-intl';
-import {View, Text, Pressable} from 'react-native';
+import {View, Pressable} from 'react-native';
+import {showLimitRestrictedAlert} from '@calls/alerts';
import leaveAndJoinWithAlert from '@calls/components/leave_and_join_alert';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
@@ -15,6 +16,7 @@ import {JOIN_CALL_BAR_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import type {LimitRestrictedInfo} from '@calls/observers';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
@@ -25,48 +27,61 @@ type Props = {
participants: UserModel[];
currentCallChannelName: string;
channelCallStartTime: number;
+ limitRestrictedInfo: LimitRestrictedInfo;
}
-const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
- return {
- container: {
- flexDirection: 'row',
- backgroundColor: '#3DB887',
- width: '100%',
- padding: 5,
- justifyContent: 'center',
- alignItems: 'center',
- height: JOIN_CALL_BAR_HEIGHT,
- },
- joinCallIcon: {
- color: theme.sidebarText,
- marginLeft: 10,
- marginRight: 5,
- },
- joinCall: {
- color: theme.sidebarText,
- fontWeight: 'bold',
- fontSize: 16,
- },
- started: {
- flex: 1,
- color: theme.sidebarText,
- fontWeight: '400',
- marginLeft: 10,
- },
- avatars: {
- marginRight: 5,
- },
- headerText: {
- color: changeOpacity(theme.centerChannelColor, 0.56),
- fontSize: 12,
- fontWeight: '600',
- paddingHorizontal: 16,
- paddingVertical: 0,
- top: 16,
- },
- };
-});
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ outerContainer: {
+ backgroundColor: theme.centerChannelBg,
+ },
+ innerContainer: {
+ flexDirection: 'row',
+ backgroundColor: '#3DB887',
+ width: '100%',
+ padding: 5,
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: JOIN_CALL_BAR_HEIGHT,
+ },
+ innerContainerRestricted: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.48),
+ },
+ joinCallIcon: {
+ color: theme.sidebarText,
+ marginLeft: 10,
+ marginRight: 5,
+ },
+ joinCall: {
+ color: theme.sidebarText,
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
+ started: {
+ flex: 1,
+ color: theme.sidebarText,
+ fontWeight: '400',
+ marginLeft: 10,
+ },
+ limitReached: {
+ flex: 1,
+ display: 'flex',
+ textAlign: 'right',
+ marginRight: 10,
+ color: '#FFFFFFD6',
+ fontWeight: '400',
+ },
+ avatars: {
+ marginRight: 5,
+ },
+ headerText: {
+ color: changeOpacity(theme.centerChannelColor, 0.56),
+ fontSize: 12,
+ fontWeight: '600',
+ paddingHorizontal: 16,
+ paddingVertical: 0,
+ top: 16,
+ },
+}));
const JoinCallBanner = ({
channelId,
@@ -76,45 +91,60 @@ const JoinCallBanner = ({
inACall,
currentCallChannelName,
channelCallStartTime,
+ limitRestrictedInfo,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const style = getStyleSheet(theme);
+ const isLimitRestricted = limitRestrictedInfo.limitRestricted;
const joinHandler = async () => {
+ if (isLimitRestricted) {
+ showLimitRestrictedAlert(limitRestrictedInfo.maxParticipants, intl);
+ return;
+ }
leaveAndJoinWithAlert(intl, serverUrl, channelId, currentCallChannelName, displayName, inACall, false);
};
return (
-
-
-
-
-
+
+
-
-
-
-
-
+ {isLimitRestricted ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
);
};
diff --git a/app/products/calls/components/leave_and_join_alert.tsx b/app/products/calls/components/leave_and_join_alert.tsx
index d6a058894b..d47517a4c2 100644
--- a/app/products/calls/components/leave_and_join_alert.tsx
+++ b/app/products/calls/components/leave_and_join_alert.tsx
@@ -73,7 +73,7 @@ export const doJoinCall = async (serverUrl: string, channelId: string, intl: Int
const res = await joinCall(serverUrl, channelId);
if (res.error) {
- const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'see server logs'});
+ const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
errorAlert(res.error?.toString() || seeLogs, intl);
}
};
diff --git a/app/products/calls/hooks.ts b/app/products/calls/hooks.ts
index 168c83e512..99f6e97490 100644
--- a/app/products/calls/hooks.ts
+++ b/app/products/calls/hooks.ts
@@ -53,7 +53,7 @@ export const useTryCallsFunction = (fn: () => void) => {
});
const notAvailable = intl.formatMessage({
id: 'mobile.calls_not_available_option',
- defaultMessage: '(Not Available)',
+ defaultMessage: '(Not available)',
});
Alert.alert(
diff --git a/app/products/calls/observers/index.ts b/app/products/calls/observers/index.ts
new file mode 100644
index 0000000000..f585c8a5ea
--- /dev/null
+++ b/app/products/calls/observers/index.ts
@@ -0,0 +1,53 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Database} from '@nozbe/watermelondb';
+import {distinctUntilChanged, switchMap, combineLatest, Observable, of as of$} from 'rxjs';
+
+import {observeCallsConfig, observeCallsState} from '@calls/state';
+import {General, License} from '@constants';
+import {observeChannel} from '@queries/servers/channel';
+import {observeLicense} from '@queries/servers/system';
+
+export const observeIsCallsFeatureRestricted = (database: Database, serverUrl: string, channelId: string) => {
+ const isCloud = observeLicense(database).pipe(
+ switchMap((l) => of$(l?.Cloud === 'true')),
+ distinctUntilChanged(),
+ );
+ const skuShortName = observeCallsConfig(serverUrl).pipe(
+ switchMap((c) => of$(c.sku_short_name)),
+ distinctUntilChanged(),
+ );
+ const isDMChannel = observeChannel(database, channelId).pipe(
+ switchMap((c) => of$(c?.type === General.DM_CHANNEL)),
+ distinctUntilChanged(),
+ );
+ return combineLatest([isCloud, skuShortName, isDMChannel]).pipe(
+ switchMap(([cloud, sku, dm]) => of$(cloud && sku === License.SKU_SHORT_NAME.Starter && !dm)), // are you restricted from making a call because of your subscription?
+ distinctUntilChanged(),
+ ) as Observable;
+};
+
+export type LimitRestrictedInfo = {
+ limitRestricted: boolean;
+ maxParticipants: number;
+}
+
+export const observeIsCallLimitRestricted = (serverUrl: string, channelId: string) => {
+ const maxParticipants = observeCallsConfig(serverUrl).pipe(
+ switchMap((c) => of$(c.MaxCallParticipants)),
+ distinctUntilChanged(),
+ );
+ const callNumOfParticipants = observeCallsState(serverUrl).pipe(
+ switchMap((cs) => of$(Object.keys(cs.calls[channelId]?.participants || {}).length)),
+ distinctUntilChanged(),
+ );
+ return combineLatest([maxParticipants, callNumOfParticipants]).pipe(
+ switchMap(([max, numParticipants]) => of$({
+ limitRestricted: max !== 0 && numParticipants >= max,
+ maxParticipants: max,
+ })),
+ distinctUntilChanged((prev, curr) =>
+ prev.limitRestricted === curr.limitRestricted && prev.maxParticipants === curr.maxParticipants),
+ ) as Observable;
+};
diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts
index a6cfb1190d..30721001b2 100644
--- a/app/products/calls/state/actions.test.ts
+++ b/app/products/calls/state/actions.test.ts
@@ -31,6 +31,7 @@ import {
setConfig,
setPluginEnabled,
} from '@calls/state/actions';
+import {License} from '@constants';
import {CallsState, CurrentCall, DefaultCallsConfig, DefaultCallsState} from '../types/calls';
@@ -632,6 +633,8 @@ describe('useCallsState', () => {
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 123,
+ sku_short_name: License.SKU_SHORT_NAME.Professional,
+ MaxCallParticipants: 8,
};
// setup
diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts
index f35bd23bef..28f0fb1550 100644
--- a/app/products/calls/state/actions.ts
+++ b/app/products/calls/state/actions.ts
@@ -11,7 +11,7 @@ import {
setChannelsWithCalls,
setCurrentCall,
} from '@calls/state';
-import {Call, ChannelsWithCalls, ServerCallsConfig} from '@calls/types/calls';
+import {Call, CallsConfig, ChannelsWithCalls} from '@calls/types/calls';
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary, enabled: Dictionary) => {
const channelsWithCalls = Object.keys(calls).reduce(
@@ -303,7 +303,7 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => {
}
};
-export const setConfig = (serverUrl: string, config: ServerCallsConfig) => {
+export const setConfig = (serverUrl: string, config: Partial) => {
const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, ...config});
};
diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts
index b8d738d295..43f76dc5a0 100644
--- a/app/products/calls/types/calls.ts
+++ b/app/products/calls/types/calls.ts
@@ -97,6 +97,8 @@ export type ServerCallsConfig = {
AllowEnableCalls: boolean;
DefaultEnabled: boolean;
NeedsTURNCredentials: boolean;
+ sku_short_name: string;
+ MaxCallParticipants: number;
}
export type CallsConfig = ServerCallsConfig & {
@@ -112,6 +114,8 @@ export const DefaultCallsConfig = {
DefaultEnabled: false,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
+ sku_short_name: '',
+ MaxCallParticipants: 0,
} as CallsConfig;
export type ICEServersConfigs = Array;
diff --git a/app/products/calls/utils.test.ts b/app/products/calls/utils.test.ts
index af0dae32aa..7a74ea1124 100644
--- a/app/products/calls/utils.test.ts
+++ b/app/products/calls/utils.test.ts
@@ -3,6 +3,8 @@
import assert from 'assert';
+import {License} from '@constants';
+
import {getICEServersConfigs} from './utils';
describe('getICEServersConfigs', () => {
@@ -13,6 +15,8 @@ describe('getICEServersConfigs', () => {
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
+ sku_short_name: License.SKU_SHORT_NAME.Professional,
+ MaxCallParticipants: 8,
};
const iceConfigs = getICEServersConfigs(config);
@@ -37,6 +41,8 @@ describe('getICEServersConfigs', () => {
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
+ sku_short_name: License.SKU_SHORT_NAME.Professional,
+ MaxCallParticipants: 8,
};
const iceConfigs = getICEServersConfigs(config);
@@ -65,6 +71,8 @@ describe('getICEServersConfigs', () => {
DefaultEnabled: true,
NeedsTURNCredentials: false,
last_retrieved_at: 0,
+ sku_short_name: License.SKU_SHORT_NAME.Professional,
+ MaxCallParticipants: 8,
};
const iceConfigs = getICEServersConfigs(config);
diff --git a/app/queries/servers/user.ts b/app/queries/servers/user.ts
index c478c615a0..784f516d39 100644
--- a/app/queries/servers/user.ts
+++ b/app/queries/servers/user.ts
@@ -2,12 +2,14 @@
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
-import {combineLatest, of as of$} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import {combineLatest, Observable, of as of$} from 'rxjs';
+import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {MM_TABLES} from '@constants/database';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
+import {observeMyChannel} from '@queries/servers/channel';
+import {isChannelAdmin} from '@utils/user';
import {queryPreferencesByCategoryAndName} from './preference';
import {observeConfig, observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense} from './system';
@@ -110,9 +112,17 @@ export const observeUserIsTeamAdmin = (database: Database, userId: string, teamI
export const observeUserIsChannelAdmin = (database: Database, userId: string, channelId: string) => {
const id = `${channelId}-${userId}`;
- return database.get(CHANNEL_MEMBERSHIP).query(
+ const myChannelRoles = observeMyChannel(database, channelId).pipe(
+ switchMap((mc) => of$(mc?.roles || '')),
+ distinctUntilChanged(),
+ );
+ const channelSchemeAdmin = database.get(CHANNEL_MEMBERSHIP).query(
Q.where('id', Q.eq(id)),
).observe().pipe(
- switchMap((tm) => of$(tm.length ? tm[0].schemeAdmin : false)),
+ switchMap((cm) => of$(cm.length ? cm[0].schemeAdmin : false)),
+ distinctUntilChanged(),
);
+ return combineLatest([myChannelRoles, channelSchemeAdmin]).pipe(
+ switchMap(([mcr, csa]) => of$(isChannelAdmin(mcr) || csa)),
+ ) as Observable;
};
diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx
index 354fbfbdb9..2029a6d1bb 100644
--- a/app/screens/channel/channel.tsx
+++ b/app/screens/channel/channel.tsx
@@ -144,9 +144,10 @@ const Channel = ({
onLayout={onLayout}
>
{shouldRender &&
<>
diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx
index d683c1132a..2f0dc648ef 100644
--- a/app/screens/channel/header/header.tsx
+++ b/app/screens/channel/header/header.tsx
@@ -38,7 +38,8 @@ type ChannelProps = {
memberCount?: number;
searchTerm: string;
teamId: string;
- callsEnabled: boolean;
+ callsEnabledInChannel: boolean;
+ callsFeatureRestricted: boolean;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -66,7 +67,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const ChannelHeader = ({
channelId, channelType, componentId, customStatus, displayName,
isCustomStatusExpired, isOwnDirectMessage, memberCount,
- searchTerm, teamId, callsEnabled,
+ searchTerm, teamId, callsEnabledInChannel, callsFeatureRestricted,
}: ChannelProps) => {
const intl = useIntl();
const isTablet = useIsTablet();
@@ -74,6 +75,7 @@ const ChannelHeader = ({
const styles = getStyleSheet(theme);
const defaultHeight = useDefaultHeaderHeight();
const insets = useSafeAreaInsets();
+ const callsAvailable = callsEnabledInChannel && !callsFeatureRestricted;
const isDMorGM = isTypeDMorGM(channelType);
const contextStyle = useMemo(() => ({
@@ -129,13 +131,13 @@ const ChannelHeader = ({
}
// When calls is enabled, we need space to move the "Copy Link" from a button to an option
- const height = QUICK_OPTIONS_HEIGHT + (callsEnabled && !isDMorGM ? ITEM_HEIGHT : 0);
+ const height = QUICK_OPTIONS_HEIGHT + (callsAvailable && !isDMorGM ? ITEM_HEIGHT : 0);
const renderContent = () => {
return (
);
@@ -148,7 +150,7 @@ const ChannelHeader = ({
theme,
closeButtonId: 'close-channel-quick-actions',
});
- }, [channelId, isDMorGM, isTablet, onTitlePress, theme, callsEnabled]);
+ }, [channelId, isDMorGM, isTablet, onTitlePress, theme, callsAvailable]);
const rightButtons: HeaderRightButton[] = useMemo(() => ([
diff --git a/app/screens/channel/header/index.ts b/app/screens/channel/header/index.ts
index a7a9d8efe7..7f03c32cbe 100644
--- a/app/screens/channel/header/index.ts
+++ b/app/screens/channel/header/index.ts
@@ -6,17 +6,27 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
+import {observeIsCallsFeatureRestricted} from '@calls/observers';
import {General} from '@constants';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
-import {getUserCustomStatus, getUserIdFromChannelName, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
+import {
+ getUserCustomStatus,
+ getUserIdFromChannelName,
+ isCustomStatusExpired as checkCustomStatusIsExpired,
+} from '@utils/user';
import ChannelHeader from './header';
import type {WithDatabaseArgs} from '@typings/database/database';
-const enhanced = withObservables(['channelId'], ({channelId, database}: WithDatabaseArgs & {channelId: string}) => {
+type OwnProps = {
+ serverUrl: string;
+ channelId: string;
+};
+
+const enhanced = withObservables(['channelId'], ({serverUrl, channelId, database}: OwnProps & WithDatabaseArgs) => {
const currentUserId = observeCurrentUserId(database);
const teamId = observeCurrentTeamId(database);
@@ -77,6 +87,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: WithData
memberCount,
searchTerm,
teamId,
+ callsFeatureRestricted: observeIsCallsFeatureRestricted(database, serverUrl, channelId),
};
});
diff --git a/app/screens/channel_info/channel_info.tsx b/app/screens/channel_info/channel_info.tsx
index 534778ea4f..2d44f1bb91 100644
--- a/app/screens/channel_info/channel_info.tsx
+++ b/app/screens/channel_info/channel_info.tsx
@@ -24,6 +24,7 @@ type Props = {
type?: ChannelType;
canEnableDisableCalls: boolean;
isCallsEnabledInChannel: boolean;
+ isCallsFeatureRestricted: boolean;
}
const edges: Edge[] = ['bottom', 'left', 'right'];
@@ -50,9 +51,11 @@ const ChannelInfo = ({
type,
canEnableDisableCalls,
isCallsEnabledInChannel,
+ isCallsFeatureRestricted,
}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
+ const callsAvailable = isCallsEnabledInChannel && !isCallsFeatureRestricted;
const onPressed = useCallback(() => {
dismissModal({componentId});
@@ -80,7 +83,7 @@ const ChannelInfo = ({
channelId={channelId}
inModal={true}
dismissChannelInfo={onPressed}
- callsEnabled={isCallsEnabledInChannel}
+ callsEnabled={callsAvailable}
testID='channel_info.channel_actions'
/>
@@ -88,10 +91,10 @@ const ChannelInfo = ({
- {canEnableDisableCalls &&
+ {canEnableDisableCalls && !isCallsFeatureRestricted &&
<>
{
}),
distinctUntilChanged(),
);
+ const isCallsFeatureRestricted = channelId.pipe(
+ switchMap((id) => observeIsCallsFeatureRestricted(database, serverUrl, id)),
+ );
return {
type,
canEnableDisableCalls,
isCallsEnabledInChannel,
+ isCallsFeatureRestricted,
};
});
diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts
index c53e427e8d..366b84a9cf 100644
--- a/app/utils/user/index.ts
+++ b/app/utils/user/index.ts
@@ -91,6 +91,10 @@ export function isSystemAdmin(roles: string): boolean {
return isRoleInRoles(roles, Permissions.SYSTEM_ADMIN_ROLE);
}
+export function isChannelAdmin(roles: string): boolean {
+ return isRoleInRoles(roles, Permissions.CHANNEL_ADMIN_ROLE);
+}
+
export const getUsersByUsername = (users: UserModel[]) => {
const usersByUsername: Dictionary = {};
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 2251c57a4e..526ab10c37 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -374,6 +374,8 @@
"mobile.calls_lasted": "Lasted {duration}",
"mobile.calls_leave": "Leave",
"mobile.calls_leave_call": "Leave call",
+ "mobile.calls_limit_msg": "The maximum number of participants per call is {maxParticipants}. Contact your System Admin to increase the limit.",
+ "mobile.calls_limit_reached": "Participant limit reached",
"mobile.calls_lower_hand": "Lower hand",
"mobile.calls_more": "More",
"mobile.calls_mute": "Mute",
@@ -381,11 +383,12 @@
"mobile.calls_name_started_call": "{name} started a call",
"mobile.calls_noone_talking": "No one is talking",
"mobile.calls_not_available_msg": "Please contact your System Admin to enable the feature.",
- "mobile.calls_not_available_option": "(Not Available)",
+ "mobile.calls_not_available_option": "(Not available)",
"mobile.calls_not_available_title": "Calls is not enabled",
"mobile.calls_ok": "OK",
+ "mobile.calls_participant_limit_title": "Participant limit reached",
"mobile.calls_raise_hand": "Raise hand",
- "mobile.calls_see_logs": "see server logs",
+ "mobile.calls_see_logs": "See server logs",
"mobile.calls_speaker": "Speaker",
"mobile.calls_start_call": "Start Call",
"mobile.calls_unmute": "Unmute",