[MM-57707] - Calls: Add host controls menu (#7905)

* add host controls screen; add control for make host

* licensing & host logic

* refactor participant card; dynamic profile sizing; PR comments

* render participant name and profile changes in ParticipantsList

* package-lock update after rebase onto main

* use config's HostControlsAllowed
This commit is contained in:
Christopher Poile 2024-04-24 17:23:23 -04:00 committed by GitHub
parent c5e6e34fea
commit c071eaf617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 432 additions and 85 deletions

View file

@ -19,6 +19,13 @@ const MultiSessionCallsVersion = {
PATCH_VERSION: 0,
};
const HostControlsCallsVersion = {
FULL_VERSION: '0.27.0',
MAJOR_VERSION: 0,
MIN_VERSION: 27,
PATCH_VERSION: 0,
};
const PluginId = 'com.mattermost.calls';
const REACTION_TIMEOUT = 10000;
@ -40,6 +47,7 @@ export default {
RefreshConfigMillis,
RequiredServer,
MultiSessionCallsVersion,
HostControlsCallsVersion,
PluginId,
REACTION_TIMEOUT,
REACTION_LIMIT,

View file

@ -8,6 +8,7 @@ export const BOTTOM_SHEET = 'BottomSheet';
export const BROWSE_CHANNELS = 'BrowseChannels';
export const CALL = 'Call';
export const CALL_PARTICIPANTS = 'CallParticipants';
export const CALL_HOST_CONTROLS = 'CallHostControls';
export const CHANNEL = 'Channel';
export const CHANNEL_ADD_MEMBERS = 'ChannelAddMembers';
export const CHANNEL_FILES = 'ChannelFiles';
@ -82,6 +83,7 @@ export default {
BROWSE_CHANNELS,
CALL,
CALL_PARTICIPANTS,
CALL_HOST_CONTROLS,
CHANNEL,
CHANNEL_ADD_MEMBERS,
CHANNEL_FILES,
@ -181,6 +183,7 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
REACTIONS,
USER_PROFILE,
CALL_PARTICIPANTS,
CALL_HOST_CONTROLS,
]);
export const NOT_READY = [

View file

@ -619,3 +619,14 @@ const handleEndCall = async (serverUrl: string, channelId: string, currentUserId
],
);
};
export const makeHost = async (serverUrl: string, callId: string, newHostId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
return client.makeHost(callId, newHostId);
} catch (error) {
logDebug('error on makeHost', getFullErrorMessage(error));
await forceLogoutIfNecessary(serverUrl, error);
return error;
}
};

View file

@ -17,6 +17,7 @@ export interface ClientCallsMix {
startCallRecording: (callId: string) => Promise<ApiResp | CallJobState>;
stopCallRecording: (callId: string) => Promise<ApiResp | CallJobState>;
dismissCall: (channelId: string) => Promise<ApiResp>;
makeHost: (callId: string, newHostId: string) => Promise<ApiResp>;
}
const ClientCalls = (superclass: any) => class extends superclass {
@ -105,6 +106,16 @@ const ClientCalls = (superclass: any) => class extends superclass {
{method: 'post'},
);
};
makeHost = async (callId: string, newHostId: string) => {
return this.doFetch(
`${this.getCallsRoute()}/calls/${callId}/host/make`,
{
method: 'post',
body: {new_host_id: newHostId},
},
);
};
};
export default ClientCalls;

View file

@ -10,6 +10,7 @@ import Permissions from 'react-native-permissions';
import {initializeVoiceTrack} from '@calls/actions/calls';
import {
getCallsConfig, getCurrentCall,
setMicPermissionsGranted,
useCallsState,
useChannelsWithCalls,
@ -17,7 +18,8 @@ import {
useGlobalCallsState,
useIncomingCalls,
} from '@calls/state';
import {errorAlert} from '@calls/utils';
import {errorAlert, isHostControlsAllowed} from '@calls/utils';
import {Screens} from '@constants';
import {
CALL_ERROR_BAR_HEIGHT,
CALL_NOTIFICATION_BAR_HEIGHT,
@ -25,11 +27,17 @@ import {
JOIN_CALL_BAR_HEIGHT,
} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {useAppState} from '@hooks/device';
import NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers';
import {getCurrentUser} from '@queries/servers/user';
import {openAsBottomSheet} from '@screens/navigation';
import {getFullErrorMessage} from '@utils/errors';
import {isSystemAdmin} from '@utils/user';
import type {CallSession} from '@calls/types/calls';
import type {Client} from '@client/rest';
export const useTryCallsFunction = (fn: () => void) => {
@ -155,3 +163,66 @@ export const useCallsAdjustment = (serverUrl: string, channelId: string): number
(joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT + 8 : 0) +
callsIncomingAdjustment;
};
export const useHostControlsAvailable = () => {
const [isAdmin, setIsAdmin] = useState(false);
const currentCall = getCurrentCall();
const serverUrl = currentCall?.serverUrl || '';
const config = getCallsConfig(serverUrl);
const allowed = isHostControlsAllowed(config);
useEffect(() => {
const getUser = async () => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const user = await getCurrentUser(database);
setIsAdmin(isSystemAdmin(user?.roles || ''));
};
getUser();
}, [serverUrl]);
const isHost = currentCall?.hostId === currentCall?.myUserId;
return allowed && (isHost || isAdmin);
};
export const useHostMenus = () => {
const intl = useIntl();
const theme = useTheme();
const currentCall = useCurrentCall();
const hostControlsAvailable = useHostControlsAvailable();
const isHost = currentCall?.hostId === currentCall?.myUserId;
const openHostControl = useCallback(async (session: CallSession) => {
const screen = Screens.CALL_HOST_CONTROLS;
const title = intl.formatMessage({id: 'mobile.calls_host_controls', defaultMessage: 'Host controls'});
const closeHostControls = 'close-host-controls';
const props = {closeButtonId: closeHostControls, session};
openAsBottomSheet({screen, title, theme, closeButtonId: closeHostControls, props});
}, [theme]);
const openUserProfile = useCallback(async (session: CallSession) => {
const screen = Screens.USER_PROFILE;
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const closeUserProfile = 'close-user-profile';
const props = {closeButtonId: closeUserProfile, location: '', userId: session.userId};
openAsBottomSheet({screen, title, theme, closeButtonId: closeUserProfile, props});
}, [theme, currentCall?.channelId]);
const onPress = useCallback((session: CallSession) => () => {
// Show host controls when allowed and I'm host or admin,
// but don't show if this is me and I'm the host already.
const isYou = session.userId === currentCall?.myUserId;
if (hostControlsAvailable && !(isYou && isHost)) {
openHostControl(session);
} else {
openUserProfile(session);
}
}, [currentCall?.myUserId, hostControlsAvailable, isHost, openHostControl, openUserProfile]);
return {hostControlsAvailable, onPress, openUserProfile};
};

View file

@ -26,7 +26,6 @@ import {leaveCall, muteMyself, unmuteMyself} from '@calls/actions';
import {startCallRecording, stopCallRecording} from '@calls/actions/calls';
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
import {AudioDeviceButton} from '@calls/components/audio_device_button';
import CallAvatar from '@calls/components/call_avatar';
import CallDuration from '@calls/components/call_duration';
import CallNotification from '@calls/components/call_notification';
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
@ -35,7 +34,8 @@ import EmojiList from '@calls/components/emoji_list';
import MessageBar from '@calls/components/message_bar';
import ReactionBar from '@calls/components/reaction_bar';
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
import {usePermissionsChecker} from '@calls/hooks';
import {useHostMenus, usePermissionsChecker} from '@calls/hooks';
import {ParticipantCard} from '@calls/screens/call_screen/participant_card';
import {RaisedHandBanner} from '@calls/screens/call_screen/raised_hand_banner';
import {
setCallQualityAlertDismissed,
@ -74,10 +74,10 @@ import {displayUsername} from '@utils/user';
import type {CallSession, CallsTheme, CurrentCall} from '@calls/types/calls';
import type {AvailableScreens} from '@typings/screens/navigation';
const avatarL = 96;
const avatarM = 72;
const usernameL = 110;
const usernameM = 92;
export const avatarL = 96;
export const avatarM = 72;
export const usernameL = 110;
export const usernameM = 92;
export type Props = {
componentId: AvailableScreens;
@ -170,29 +170,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
users: {
flexDirection: 'row',
flexWrap: 'wrap',
},
user: {
flexGrow: 1,
flexDirection: 'column',
alignItems: 'center',
margin: 10,
},
userScreenOn: {
marginTop: 5,
marginBottom: 0,
},
profileScreenOn: {
marginBottom: 2,
},
username: {
width: usernameL,
textAlign: 'center',
color: theme.buttonColor,
...typography('Body', 100, 'SemiBold'),
},
usernameShort: {
marginTop: 0,
width: usernameM,
justifyContent: 'center',
},
buttonsContainer: {
alignItems: 'center',
@ -256,6 +234,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
otherButtons: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
},
otherButtonsLandscape: {
justifyContent: 'center',
@ -277,10 +256,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
buttonIcon: {
color: theme.buttonColor,
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
borderRadius: 34,
padding: 18,
width: 68,
height: 68,
borderRadius: 30,
padding: 14,
width: 60,
height: 60,
marginBottom: 8,
overflow: 'hidden',
},
@ -334,6 +313,7 @@ const CallScreen = ({
const {EnableRecordings, EnableTranscriptions} = useCallsConfig(serverUrl);
usePermissionsChecker(micPermissionsGranted);
const incomingCalls = useIncomingCalls();
const {hostControlsAvailable, onPress, openUserProfile} = useHostMenus();
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
const [showReactions, setShowReactions] = useState(false);
@ -342,12 +322,14 @@ const CallScreen = ({
const style = getStyleSheet(callsTheme);
const [centerUsers, setCenterUsers] = useState(false);
const [layout, setLayout] = useState<LayoutRectangle | null>(null);
const [contentOverflow, setContentOverflow] = useState(false);
const [previousNumSessions, setPreviousNumSessions] = useState(0);
const mySession = currentCall?.sessions[currentCall.mySessionId];
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
const screenShareOn = Boolean(currentCall?.screenOn);
const isLandscape = width > height;
const smallerAvatar = isLandscape || screenShareOn || showCC;
const smallerAvatar = isLandscape || screenShareOn || showCC || contentOverflow;
const avatarSize = smallerAvatar ? avatarM : avatarL;
const numSessions = Object.keys(sessionsDict).length;
const showIncomingCalls = incomingCalls.incomingCalls.length > 0;
@ -591,6 +573,28 @@ const CallScreen = ({
setLayout(e.nativeEvent.layout);
}, []);
const onContentSizeChange = useCallback((_: number, h: number) => {
// If numSessions has changed, perform contentOverflow check. Prevents infinite loop.
if (numSessions !== previousNumSessions) {
setContentOverflow(h > (layout?.height || 0));
setPreviousNumSessions(numSessions);
}
}, [layout, numSessions, previousNumSessions]);
const onShortPress = useCallback((session: CallSession) => () => {
if (hostControlsAvailable) {
onPress(session)();
}
}, [hostControlsAvailable, onPress]);
const onLongPress = useCallback((session: CallSession) => () => {
if (hostControlsAvailable) {
onPress(session)();
} else {
openUserProfile(session);
}
}, [hostControlsAvailable, onPress, openUserProfile]);
if (!currentCall || !mySession) {
return null;
}
@ -629,6 +633,7 @@ const CallScreen = ({
alwaysBounceVertical={false}
horizontal={screenShareOn}
onLayout={onLayout}
onContentSizeChange={onContentSizeChange}
contentContainerStyle={centerUsers && style.usersScrollViewCentered}
>
<Pressable
@ -636,37 +641,16 @@ const CallScreen = ({
onPress={toggleControlsInLandscape}
style={style.users}
>
{sessions.map((sess) => {
return (
<View
style={[style.user, screenShareOn && style.userScreenOn]}
key={sess.sessionId}
>
<View style={[screenShareOn && style.profileScreenOn]}>
<CallAvatar
userModel={sess.userModel}
speaking={currentCall.voiceOn[sess.sessionId]}
muted={sess.muted}
sharingScreen={sess.sessionId === currentCall.screenOn}
raisedHand={Boolean(sess.raisedHand)}
reaction={sess.reaction?.emoji}
size={avatarSize}
serverUrl={currentCall.serverUrl}
/>
</View>
<Text
style={[style.username, smallerAvatar && style.usernameShort]}
numberOfLines={1}
>
{displayUsername(sess.userModel, intl.locale, teammateNameDisplay)}
{sess.sessionId === mySession.sessionId &&
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
}
</Text>
{sess.userId === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
</View>
);
})}
{sessions.map((sess) => (
<ParticipantCard
key={sess.sessionId}
session={sess}
smallerAvatar={smallerAvatar}
teammateNameDisplay={teammateNameDisplay}
onPress={onShortPress(sess)}
onLongPress={onLongPress(sess)}
/>
))}
</Pressable>
</ScrollView>
</View>

View file

@ -0,0 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Pressable, Text, View} from 'react-native';
import CallAvatar from '@calls/components/call_avatar';
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
import {avatarL, avatarM, usernameL, usernameM} from '@calls/screens/call_screen/call_screen';
import {useCurrentCall} from '@calls/state';
import {makeCallsTheme} from '@calls/utils';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type {CallSession, CallsTheme} from '@calls/types/calls';
type Props = {
session: CallSession;
smallerAvatar: boolean;
teammateNameDisplay: string;
onPress: () => void;
onLongPress: () => void;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
user: {
flexDirection: 'column',
alignItems: 'center',
margin: 4,
padding: 12,
borderRadius: 8,
},
pressed: {
backgroundColor: changeOpacity(theme.sidebarText, 0.08),
},
userScreenOn: {
marginTop: 5,
marginBottom: 0,
},
profileScreenOn: {
marginBottom: 2,
},
username: {
width: usernameL,
textAlign: 'center',
color: theme.buttonColor,
...typography('Body', 100, 'SemiBold'),
},
usernameShort: {
marginTop: 0,
width: usernameM,
},
}));
export const ParticipantCard = ({session, smallerAvatar, teammateNameDisplay, onPress, onLongPress}: Props) => {
const intl = useIntl();
const theme = useTheme();
const currentCall = useCurrentCall();
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
const style = getStyleSheet(callsTheme);
const mySession = currentCall?.sessions[currentCall.mySessionId];
const screenShareOn = Boolean(currentCall?.screenOn);
const avatarSize = smallerAvatar ? avatarM : avatarL;
if (!currentCall || !mySession) {
return null;
}
return (
<Pressable
onPress={onPress}
onLongPress={onLongPress}
>
{({pressed}) => (
<View
style={[style.user, pressed && style.pressed, screenShareOn && style.userScreenOn]}
key={session.sessionId}
>
<View style={[screenShareOn && style.profileScreenOn]}>
<CallAvatar
userModel={session.userModel}
speaking={currentCall.voiceOn[session.sessionId]}
muted={session.muted}
sharingScreen={session.sessionId === currentCall.screenOn}
raisedHand={Boolean(session.raisedHand)}
reaction={session.reaction?.emoji}
size={avatarSize}
serverUrl={currentCall.serverUrl}
/>
</View>
<Text
style={[style.username, smallerAvatar && style.usernameShort]}
numberOfLines={1}
>
{displayUsername(session.userModel, intl.locale, teammateNameDisplay)}
{session.sessionId === mySession.sessionId &&
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
}
</Text>
{session.userId === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
</View>
)}
</Pressable>
);
};

View file

@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {makeHost} from '@calls/actions/calls';
import SlideUpPanelItem from '@components/slide_up_panel_item';
import {Screens} from '@constants';
import BottomSheet from '@screens/bottom_sheet';
import {dismissBottomSheet} from '@screens/navigation';
import UserProfileTitle from '@screens/user_profile/title';
import {bottomSheetSnapPoint} from '@utils/helpers';
import type {CallSession, CurrentCall} from '@calls/types/calls';
const TITLE_HEIGHT = 118;
const ITEM_HEIGHT = 48;
type Props = {
currentCall: CurrentCall;
closeButtonId: string;
session: CallSession;
teammateNameDisplay: string;
hideGuestTags: boolean;
}
const styles = StyleSheet.create({
iconStyle: {
marginRight: 6,
},
});
export const HostControls = ({
currentCall,
closeButtonId,
session,
teammateNameDisplay,
hideGuestTags,
}: Props) => {
const intl = useIntl();
const {bottom} = useSafeAreaInsets();
const makeHostPress = useCallback(async () => {
await makeHost(currentCall.serverUrl, currentCall.channelId, session.userId);
await dismissBottomSheet();
}, [currentCall, session.userId]);
const snapPoints = useMemo(() => {
return [
1,
bottomSheetSnapPoint(1, ITEM_HEIGHT, bottom) + TITLE_HEIGHT,
];
}, [bottom]);
const makeHostText = intl.formatMessage({id: 'mobile.calls_make_host', defaultMessage: 'Make host'});
const renderContent = () => {
if (!session?.userModel) {
return null;
}
return (
<>
<UserProfileTitle
enablePostIconOverride={false}
enablePostUsernameOverride={false}
isChannelAdmin={false}
isSystemAdmin={false}
isTeamAdmin={false}
teammateDisplayName={teammateNameDisplay}
user={session.userModel}
hideGuestTags={hideGuestTags}
/>
<SlideUpPanelItem
leftIcon={'monitor-account'}
leftIconStyles={styles.iconStyle}
onPress={makeHostPress}
text={makeHostText}
/>
</>
);
};
return (
<BottomSheet
renderContent={renderContent}
closeButtonId={closeButtonId}
componentId={Screens.CALL_HOST_CONTROLS}
initialSnapIndex={1}
snapPoints={snapPoints}
/>
);
};

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeCallDatabase, observeCurrentSessionsDict} from '@calls/observers';
import {HostControls} from '@calls/screens/host_controls/host_controls';
import {observeCurrentCall} from '@calls/state';
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
const enhanced = withObservables([], () => {
const teammateNameDisplay = observeCallDatabase().pipe(
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
distinctUntilChanged(),
);
const hideGuestTags = observeCallDatabase().pipe(
switchMap((db) => (db ? observeConfigBooleanValue(db, 'HideGuestTags') : of$(false))),
distinctUntilChanged(),
);
return {
currentCall: observeCurrentCall(),
sessionsDict: observeCurrentSessionsDict(),
teammateNameDisplay,
hideGuestTags,
};
});
export default enhanced(HostControls);

View file

@ -3,7 +3,7 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {Platform, Pressable, Text, View} from 'react-native';
import {Platform, Text, TouchableOpacity, View} from 'react-native';
import {useCurrentCall} from '@calls/state';
import CompassIcon from '@components/compass_icon';
@ -22,6 +22,7 @@ const PROFILE_SIZE = 32;
type Props = {
sess: CallSession;
teammateNameDisplay: string;
onPress: () => void;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@ -43,12 +44,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
width: PROFILE_SIZE,
},
name: {
...typography('Body', 200, 'SemiBold'),
...typography('Body', 200),
color: theme.centerChannelColor,
flex: 1,
},
you: {
...typography('Body', 200, 'SemiBold'),
...typography('Body', 200),
color: changeOpacity(theme.centerChannelColor, 0.56),
},
profileIcon: {
@ -75,7 +76,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
export const Participant = ({sess, teammateNameDisplay}: Props) => {
export const Participant = ({sess, teammateNameDisplay, onPress}: Props) => {
const intl = useIntl();
const currentCall = useCurrentCall();
const theme = useTheme();
@ -87,10 +88,11 @@ export const Participant = ({sess, teammateNameDisplay}: Props) => {
}
return (
<Pressable
<TouchableOpacity
key={sess.sessionId}
testID='users-list'
style={styles.rowContainer}
onPress={onPress}
>
{sess.userModel ? (
<ProfilePicture
@ -154,6 +156,6 @@ export const Participant = ({sess, teammateNameDisplay}: Props) => {
style={sess.muted ? styles.muteIcon : styles.unmutedIcon}
/>
</View>
</Pressable>
</TouchableOpacity>
);
};

View file

@ -8,6 +8,7 @@ import {type ListRenderItemInfo, useWindowDimensions, View} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {useHostMenus} from '@calls/hooks';
import {Participant} from '@calls/screens/participants_list/participant';
import Pill from '@calls/screens/participants_list/pill';
import {sortSessions} from '@calls/utils';
@ -48,13 +49,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDisplay}: Props) => {
const intl = useIntl();
const theme = useTheme();
const {onPress} = useHostMenus();
const {bottom} = useSafeAreaInsets();
const {height} = useWindowDimensions();
const isTablet = useIsTablet();
const List = useMemo(() => (isTablet ? FlatList : BottomSheetFlatList), [isTablet]);
const styles = getStyleSheet(theme);
const sessions = useMemo(() => sortSessions(intl.locale, teammateNameDisplay, sessionsDict), [sessionsDict]);
const sessions = sortSessions(intl.locale, teammateNameDisplay, sessionsDict);
const snapPoint1 = bottomSheetSnapPoint(Math.min(sessions.length, MIN_ROWS), ROW_HEIGHT, bottom) + HEADER_HEIGHT;
const snapPoint2 = height * 0.8;
const snapPoints = [1, Math.min(snapPoint1, snapPoint2)];
@ -67,8 +69,9 @@ export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDispl
key={item.sessionId}
sess={item}
teammateNameDisplay={teammateNameDisplay}
onPress={onPress(item)}
/>
), [teammateNameDisplay]);
), [teammateNameDisplay, onPress]);
const renderContent = () => {
return (

View file

@ -167,6 +167,7 @@ export const DefaultCallsConfig: CallsConfigState = {
EnableRinging: false,
EnableTranscriptions: false,
EnableLiveCaptions: false,
HostControlsAllowed: false,
};
export type ApiResp = {

View file

@ -11,7 +11,14 @@ import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification';
import {isMinimumServerVersion} from '@utils/helpers';
import {displayUsername} from '@utils/user';
import type {CallSession, CallsTheme, CallsVersion, SelectedSubtitleTrack, SubtitleTrack} from '@calls/types/calls';
import type {
CallsConfigState,
CallSession,
CallsTheme,
CallsVersion,
SelectedSubtitleTrack,
SubtitleTrack,
} from '@calls/types/calls';
import type {CallsConfig, Caption} from '@mattermost/calls/lib/types';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
@ -99,6 +106,10 @@ export function isMultiSessionSupported(callsVersion: CallsVersion) {
);
}
export function isHostControlsAllowed(config: CallsConfigState) {
return Boolean(config.HostControlsAllowed);
}
export function isCallsCustomMessage(post: PostModel | Post): boolean {
return Boolean(post.type && post.type === Post.POST_TYPES.CUSTOM_CALLS);
}

View file

@ -271,6 +271,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.CALL_PARTICIPANTS:
screen = withServerDatabase(require('@calls/screens/participants_list').default);
break;
case Screens.CALL_HOST_CONTROLS:
screen = withServerDatabase(require('@calls/screens/host_controls').default);
break;
}
if (screen) {

View file

@ -463,6 +463,7 @@
"mobile.calls_headset": "Headset",
"mobile.calls_hide_cc": "Hide live captions",
"mobile.calls_host": "host",
"mobile.calls_host_controls": "Host controls",
"mobile.calls_host_rec": "You are recording this meeting. Consider letting everyone know that this meeting is being recorded.",
"mobile.calls_host_rec_error": "Please try to record again. You can also contact your system admin for troubleshooting help.",
"mobile.calls_host_rec_error_title": "Something went wrong with the recording",
@ -481,6 +482,7 @@
"mobile.calls_limit_msg_GA": "Upgrade to Cloud Professional or Cloud Enterprise to enable group calls with more than {maxParticipants} participants.",
"mobile.calls_limit_reached": "Participant limit reached",
"mobile.calls_lower_hand": "Lower hand",
"mobile.calls_make_host": "Make host",
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
"mobile.calls_more": "More",
"mobile.calls_mute": "Mute",

16
package-lock.json generated
View file

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "mattermost-mobile",
"version": "2.15.0",
"version": "2.16.0",
"hasInstallScript": true,
"license": "Apache 2.0",
"dependencies": {
@ -17,8 +17,8 @@
"@formatjs/intl-numberformat": "8.10.1",
"@formatjs/intl-pluralrules": "5.2.12",
"@gorhom/bottom-sheet": "4.6.1",
"@mattermost/calls": "github:mattermost/calls-common#954d125d14fd3b46d723bab01d92a0ef3ecca1b8",
"@mattermost/compass-icons": "0.1.43",
"@mattermost/calls": "github:mattermost/calls-common#cc5e65f6c103138fd013d6fa45eb33bfe7544f2a",
"@mattermost/compass-icons": "0.1.44",
"@mattermost/react-native-emm": "1.4.1",
"@mattermost/react-native-network-client": "1.5.1",
"@mattermost/react-native-paste-input": "0.7.1",
@ -3481,8 +3481,8 @@
"node_modules/@mattermost/calls": {
"name": "@calls/common",
"version": "0.14.0",
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#954d125d14fd3b46d723bab01d92a0ef3ecca1b8",
"integrity": "sha512-dPuvLiWc33WhhTTDqqcurstsh7A3VoGhiAHugQ5rWidvrG1OkzshzTj3jJ0dlCGIUR9EORe+m7M+XeyGgR6KvA=="
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#cc5e65f6c103138fd013d6fa45eb33bfe7544f2a",
"integrity": "sha512-fWCVnjuU3LQ8RO35uI9Qypi7HSVWoALcbNIotNcWd52XQ+BLgt69nSpKRli9qbvj4d9W/UgPaT3bOsyBvIXQrA=="
},
"node_modules/@mattermost/commonmark": {
"version": "0.30.1-2",
@ -3504,9 +3504,9 @@
}
},
"node_modules/@mattermost/compass-icons": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.43.tgz",
"integrity": "sha512-vQThJ4SAynnS2u94lQtZ9xANsStpVh8uTpsJascHJOWcavLuL2aDmMLgvg9EAx8Z1qRmTdP6hF5+IU5+9E9+Jg=="
"version": "0.1.44",
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.44.tgz",
"integrity": "sha512-cAERA4bzbNcKbVAYdAxVXOhKpmRvs6IUom/XojEg1H7Pyz1g12D+wab849TEpANeyc5j65cjM4oUBO+KYHwpyA=="
},
"node_modules/@mattermost/react-native-emm": {
"version": "1.4.1",

View file

@ -18,8 +18,8 @@
"@formatjs/intl-numberformat": "8.10.1",
"@formatjs/intl-pluralrules": "5.2.12",
"@gorhom/bottom-sheet": "4.6.1",
"@mattermost/calls": "github:mattermost/calls-common#954d125d14fd3b46d723bab01d92a0ef3ecca1b8",
"@mattermost/compass-icons": "0.1.43",
"@mattermost/calls": "github:mattermost/calls-common#cc5e65f6c103138fd013d6fa45eb33bfe7544f2a",
"@mattermost/compass-icons": "0.1.44",
"@mattermost/react-native-emm": "1.4.1",
"@mattermost/react-native-network-client": "1.5.1",
"@mattermost/react-native-paste-input": "0.7.1",