[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:
parent
c5e6e34fea
commit
c071eaf617
17 changed files with 432 additions and 85 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
109
app/products/calls/screens/call_screen/participant_card.tsx
Normal file
109
app/products/calls/screens/call_screen/participant_card.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
96
app/products/calls/screens/host_controls/host_controls.tsx
Normal file
96
app/products/calls/screens/host_controls/host_controls.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
32
app/products/calls/screens/host_controls/index.ts
Normal file
32
app/products/calls/screens/host_controls/index.ts
Normal 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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export const DefaultCallsConfig: CallsConfigState = {
|
|||
EnableRinging: false,
|
||||
EnableTranscriptions: false,
|
||||
EnableLiveCaptions: false,
|
||||
HostControlsAllowed: false,
|
||||
};
|
||||
|
||||
export type ApiResp = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
16
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue