diff --git a/app/constants/calls.ts b/app/constants/calls.ts index 027d02f94..43ecb2676 100644 --- a/app/constants/calls.ts +++ b/app/constants/calls.ts @@ -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, diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 4acce8fd1..9d53d9425 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -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([ REACTIONS, USER_PROFILE, CALL_PARTICIPANTS, + CALL_HOST_CONTROLS, ]); export const NOT_READY = [ diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index c2b249bc4..3677ae428 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -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; + } +}; diff --git a/app/products/calls/client/rest.ts b/app/products/calls/client/rest.ts index 6e3fb85d5..8d914983e 100644 --- a/app/products/calls/client/rest.ts +++ b/app/products/calls/client/rest.ts @@ -17,6 +17,7 @@ export interface ClientCallsMix { startCallRecording: (callId: string) => Promise; stopCallRecording: (callId: string) => Promise; dismissCall: (channelId: string) => Promise; + makeHost: (callId: string, newHostId: string) => Promise; } 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; diff --git a/app/products/calls/hooks.ts b/app/products/calls/hooks.ts index c319912a9..c0f1f98f9 100644 --- a/app/products/calls/hooks.ts +++ b/app/products/calls/hooks.ts @@ -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}; +}; diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index 50e90921b..ae3847a75 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -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(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} > - {sessions.map((sess) => { - return ( - - - - - - {displayUsername(sess.userModel, intl.locale, teammateNameDisplay)} - {sess.sessionId === mySession.sessionId && - ` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}` - } - - {sess.userId === currentCall.hostId && } - - ); - })} + {sessions.map((sess) => ( + + ))} diff --git a/app/products/calls/screens/call_screen/participant_card.tsx b/app/products/calls/screens/call_screen/participant_card.tsx new file mode 100644 index 000000000..77afa0cbb --- /dev/null +++ b/app/products/calls/screens/call_screen/participant_card.tsx @@ -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 ( + + {({pressed}) => ( + + + + + + {displayUsername(session.userModel, intl.locale, teammateNameDisplay)} + {session.sessionId === mySession.sessionId && + ` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}` + } + + {session.userId === currentCall.hostId && } + + )} + + ); +}; diff --git a/app/products/calls/screens/host_controls/host_controls.tsx b/app/products/calls/screens/host_controls/host_controls.tsx new file mode 100644 index 000000000..743ce419b --- /dev/null +++ b/app/products/calls/screens/host_controls/host_controls.tsx @@ -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 ( + <> + + + + ); + }; + + return ( + + ); +}; diff --git a/app/products/calls/screens/host_controls/index.ts b/app/products/calls/screens/host_controls/index.ts new file mode 100644 index 000000000..eb8d8094d --- /dev/null +++ b/app/products/calls/screens/host_controls/index.ts @@ -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); diff --git a/app/products/calls/screens/participants_list/participant.tsx b/app/products/calls/screens/participants_list/participant.tsx index 2525c3f0a..373d3d572 100644 --- a/app/products/calls/screens/participants_list/participant.tsx +++ b/app/products/calls/screens/participants_list/participant.tsx @@ -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 ( - {sess.userModel ? ( { style={sess.muted ? styles.muteIcon : styles.unmutedIcon} /> - + ); }; diff --git a/app/products/calls/screens/participants_list/participants_list.tsx b/app/products/calls/screens/participants_list/participants_list.tsx index 824706524..f0200d0e4 100644 --- a/app/products/calls/screens/participants_list/participants_list.tsx +++ b/app/products/calls/screens/participants_list/participants_list.tsx @@ -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 ( diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts index 1886372b2..8bdac60c6 100644 --- a/app/products/calls/types/calls.ts +++ b/app/products/calls/types/calls.ts @@ -167,6 +167,7 @@ export const DefaultCallsConfig: CallsConfigState = { EnableRinging: false, EnableTranscriptions: false, EnableLiveCaptions: false, + HostControlsAllowed: false, }; export type ApiResp = { diff --git a/app/products/calls/utils.ts b/app/products/calls/utils.ts index 04d554b0a..c15d34fbf 100644 --- a/app/products/calls/utils.ts +++ b/app/products/calls/utils.ts @@ -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); } diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 80286c143..2283c7798 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -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) { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 8ca853605..86d7a3d27 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 2869e4d2e..b90d9f081 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 96413f788..0aa7955f1 100644 --- a/package.json +++ b/package.json @@ -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",