forked from Ivasoft/mattermost-mobile
MM-45747 - Cloud freemium limits & Max participants limits (#6578)
This commit is contained in:
committed by
GitHub
parent
1692687c32
commit
d30b97ba99
@@ -10,6 +10,14 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -277,6 +277,7 @@ const Post = ({
|
||||
} else if (isCallsPost && !hasBeenDeleted) {
|
||||
body = (
|
||||
<CallsCustomMessage
|
||||
serverUrl={serverUrl}
|
||||
post={post}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
12
app/constants/license.ts
Normal file
12
app/constants/license.ts
Normal file
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
32
app/products/calls/alerts.ts
Normal file
32
app/products/calls/alerts.ts
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<View style={style.messageStyle}>
|
||||
<CompassIcon
|
||||
@@ -179,20 +198,20 @@ export const CallsCustomMessage = ({
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={style.joinCallButton}
|
||||
style={[style.joinCallButton, isLimitRestricted && style.joinCallButtonRestricted]}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-outline'
|
||||
size={16}
|
||||
style={style.joinCallButtonIcon}
|
||||
style={[style.joinCallButtonIcon, isLimitRestricted && style.joinCallButtonIconRestricted]}
|
||||
/>
|
||||
{
|
||||
alreadyInTheCall &&
|
||||
<FormattedText
|
||||
id={'mobile.calls_current_call'}
|
||||
defaultMessage={'Current call'}
|
||||
style={style.joinCallButtonText}
|
||||
style={joinTextStyle}
|
||||
/>
|
||||
}
|
||||
{
|
||||
@@ -200,7 +219,7 @@ export const CallsCustomMessage = ({
|
||||
<FormattedText
|
||||
id={'mobile.calls_join_call'}
|
||||
defaultMessage={'Join call'}
|
||||
style={style.joinCallButtonText}
|
||||
style={joinTextStyle}
|
||||
/>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Pressable
|
||||
style={style.container}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_join_call'}
|
||||
defaultMessage={'Join call'}
|
||||
style={style.joinCall}
|
||||
/>
|
||||
<Text style={style.started}>
|
||||
<FormattedRelativeTime
|
||||
value={channelCallStartTime}
|
||||
updateIntervalInSeconds={1}
|
||||
<View style={style.outerContainer}>
|
||||
<Pressable
|
||||
style={[style.innerContainer, isLimitRestricted && style.innerContainerRestricted]}
|
||||
onPress={joinHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.joinCallIcon}
|
||||
/>
|
||||
</Text>
|
||||
<View style={style.avatars}>
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={Screens.CHANNEL}
|
||||
users={participants}
|
||||
breakAt={1}
|
||||
<FormattedText
|
||||
id={'mobile.calls_join_call'}
|
||||
defaultMessage={'Join call'}
|
||||
style={style.joinCall}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
{isLimitRestricted ? (
|
||||
<FormattedText
|
||||
id={'mobile.calls_limit_reached'}
|
||||
defaultMessage={'Participant limit reached'}
|
||||
style={style.limitReached}
|
||||
/>
|
||||
) : (
|
||||
<FormattedRelativeTime
|
||||
value={channelCallStartTime}
|
||||
updateIntervalInSeconds={1}
|
||||
style={style.started}
|
||||
/>
|
||||
)}
|
||||
<View style={style.avatars}>
|
||||
<UserAvatarsStack
|
||||
channelId={channelId}
|
||||
location={Screens.CHANNEL}
|
||||
users={participants}
|
||||
breakAt={1}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
53
app/products/calls/observers/index.ts
Normal file
53
app/products/calls/observers/index.ts
Normal file
@@ -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<boolean>;
|
||||
};
|
||||
|
||||
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<LimitRestrictedInfo>;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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<Call>, enabled: Dictionary<boolean>) => {
|
||||
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<CallsConfig>) => {
|
||||
const callsConfig = getCallsConfig(serverUrl);
|
||||
setCallsConfig(serverUrl, {...callsConfig, ...config});
|
||||
};
|
||||
|
||||
@@ -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<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
|
||||
const myChannelRoles = observeMyChannel(database, channelId).pipe(
|
||||
switchMap((mc) => of$(mc?.roles || '')),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const channelSchemeAdmin = database.get<ChannelMembershipModel>(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<boolean>;
|
||||
};
|
||||
|
||||
@@ -144,9 +144,10 @@ const Channel = ({
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<ChannelHeader
|
||||
serverUrl={serverUrl}
|
||||
channelId={channelId}
|
||||
componentId={componentId}
|
||||
callsEnabled={isCallsEnabledInChannel}
|
||||
callsEnabledInChannel={isCallsEnabledInChannel}
|
||||
/>
|
||||
{shouldRender &&
|
||||
<>
|
||||
|
||||
@@ -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 (
|
||||
<QuickActions
|
||||
channelId={channelId}
|
||||
callsEnabled={callsEnabled}
|
||||
callsEnabled={callsAvailable}
|
||||
isDMorGM={isDMorGM}
|
||||
/>
|
||||
);
|
||||
@@ -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(() => ([
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
<Extra channelId={channelId}/>
|
||||
@@ -88,10 +91,10 @@ const ChannelInfo = ({
|
||||
<Options
|
||||
channelId={channelId}
|
||||
type={type}
|
||||
callsEnabled={isCallsEnabledInChannel}
|
||||
callsEnabled={callsAvailable}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
{canEnableDisableCalls &&
|
||||
{canEnableDisableCalls && !isCallsFeatureRestricted &&
|
||||
<>
|
||||
<ChannelInfoEnableCalls
|
||||
channelId={channelId}
|
||||
|
||||
@@ -6,6 +6,7 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeIsCallsFeatureRestricted} from '@calls/observers';
|
||||
import {observeCallsConfig, observeCallsState} from '@calls/state';
|
||||
import {General} from '@constants';
|
||||
import {withServerUrl} from '@context/server';
|
||||
@@ -69,11 +70,15 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isCallsFeatureRestricted = channelId.pipe(
|
||||
switchMap((id) => observeIsCallsFeatureRestricted(database, serverUrl, id)),
|
||||
);
|
||||
|
||||
return {
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
isCallsEnabledInChannel,
|
||||
isCallsFeatureRestricted,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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<UserModel> = {};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user