[MM-58129] Calls: Host controls remove participant, view profile (#7938)

* host remove command, host removed feedback

* add view profile control

* i18n; dismissBottomSheet later

* PR comments

* remove confusing setPreviousCall(null)

* our own separator, bit more space for Android

* no need for previousCall due to changes on server

* move host removed alert to an error

* channel_id is not send via the msg.broadcast

* channel_id is still sent via the msg.broadcast
This commit is contained in:
Christopher Poile 2024-05-10 16:43:35 -04:00 committed by GitHub
parent 4992a96891
commit 7fbd2e52e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 150 additions and 10 deletions

View file

@ -36,6 +36,7 @@ import {
handleCallUserVoiceOn,
handleHostLowerHand,
handleHostMute,
handleHostRemoved,
handleUserDismissedNotification,
} from '@calls/connection/websocket_event_handlers';
import {isSupportedServerCalls} from '@calls/utils';
@ -461,6 +462,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
case WebsocketEvents.CALLS_HOST_LOWER_HAND:
handleHostLowerHand(serverUrl, msg);
break;
case WebsocketEvents.CALLS_HOST_REMOVED:
handleHostRemoved(serverUrl, msg);
break;
case WebsocketEvents.GROUP_RECEIVED:
handleGroupReceivedEvent(serverUrl, msg);

View file

@ -90,6 +90,7 @@ const WebsocketEvents = {
CALLS_CAPTION: `custom_${Calls.PluginId}_caption`,
CALLS_HOST_MUTE: `custom_${Calls.PluginId}_host_mute`,
CALLS_HOST_LOWER_HAND: `custom_${Calls.PluginId}_host_lower_hand`,
CALLS_HOST_REMOVED: `custom_${Calls.PluginId}_host_removed`,
GROUP_RECEIVED: 'received_group',
GROUP_MEMBER_ADD: 'group_member_add',

View file

@ -664,3 +664,13 @@ export const hostLowerHand = async (serverUrl: string, callId: string, sessionId
}
};
export const hostRemove = async (serverUrl: string, callId: string, sessionId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
return client.hostRemove(callId, sessionId);
} catch (error) {
logDebug('error on hostRemove', getFullErrorMessage(error));
await forceLogoutIfNecessary(serverUrl, error);
return error;
}
};

View file

@ -4,9 +4,9 @@
import {Alert} from 'react-native';
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
import {dismissIncomingCall} from '@calls/actions/calls';
import {dismissIncomingCall, hostRemove} from '@calls/actions/calls';
import {hasBluetoothPermission} from '@calls/actions/permissions';
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
import {hostRemovedErr, userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
import {
getCallsConfig,
getCallsState,
@ -19,6 +19,7 @@ import {errorAlert} from '@calls/utils';
import DatabaseManager from '@database/manager';
import {getChannelById} from '@queries/servers/channel';
import {getCurrentUser} from '@queries/servers/user';
import {dismissBottomSheet} from '@screens/navigation';
import {isDMorGM} from '@utils/channel';
import {getFullErrorMessage} from '@utils/errors';
import {logError} from '@utils/log';
@ -413,8 +414,55 @@ export const showErrorAlertOnClose = (err: Error, intl: IntlShape) => {
}),
);
break;
case hostRemovedErr:
Alert.alert(
intl.formatMessage({
id: 'mobile.calls_removed_alert_title',
defaultMessage: 'You were removed from the call',
}),
intl.formatMessage({
id: 'mobile.calls_removed_alert_body',
defaultMessage: 'The host removed you from the call.',
}),
[{
text: intl.formatMessage({
id: 'mobile.calls_dismiss',
defaultMessage: 'Dismiss',
}),
}]);
break;
default:
// Fallback with generic error
errorAlert(getFullErrorMessage(err, intl), intl);
}
};
export const removeFromCall = (serverUrl: string, displayName: string, callId: string, sessionId: string, intl: IntlShape) => {
const {formatMessage} = intl;
const title = formatMessage({
id: 'mobile.calls_remove_alert_title',
defaultMessage: 'Remove participant',
});
const body = formatMessage({
id: 'mobile.calls_remove_alert_body',
defaultMessage: 'Are you sure you want to remove {displayName} from the call? ',
}, {displayName});
Alert.alert(title, body, [{
text: formatMessage({
id: 'mobile.post.cancel',
defaultMessage: 'Cancel',
}),
}, {
text: formatMessage({
id: 'mobile.calls_remove',
defaultMessage: 'Remove',
}),
onPress: () => {
hostRemove(serverUrl, callId, sessionId);
dismissBottomSheet();
},
style: 'destructive',
}]);
};

View file

@ -21,6 +21,7 @@ export interface ClientCallsMix {
hostMute: (callId: string, sessionId: string) => Promise<ApiResp>;
hostScreenOff: (callId: string, sessionId: string) => Promise<ApiResp>;
hostLowerHand: (callId: string, sessionId: string) => Promise<ApiResp>;
hostRemove: (callId: string, sessionId: string) => Promise<ApiResp>;
}
const ClientCalls = (superclass: any) => class extends superclass {
@ -149,6 +150,16 @@ const ClientCalls = (superclass: any) => class extends superclass {
},
);
};
hostRemove = async (callId: string, sessionId: string) => {
return this.doFetch(
`${this.getCallsRoute()}/calls/${callId}/host/remove`,
{
method: 'post',
body: {session_id: sessionId},
},
);
};
};
export default ClientCalls;

View file

@ -4,7 +4,8 @@
import {DeviceEventEmitter} from 'react-native';
import {fetchUsersByIds} from '@actions/remote/user';
import {muteMyself, unraiseHand} from '@calls/actions';
import {leaveCall, muteMyself, unraiseHand} from '@calls/actions';
import {hostRemovedErr} from '@calls/errors';
import {
callEnded,
callStarted,
@ -228,3 +229,14 @@ export const handleHostLowerHand = async (serverUrl: string, msg: WebSocketMessa
unraiseHand();
};
export const handleHostRemoved = async (serverUrl: string, msg: WebSocketMessage<HostControlsMsgData>) => {
const currentCall = getCurrentCall();
if (currentCall?.serverUrl !== serverUrl ||
currentCall?.channelId !== msg.data.channel_id ||
currentCall?.mySessionId !== msg.data.session_id) {
return;
}
leaveCall(hostRemovedErr);
};

View file

@ -3,3 +3,4 @@
export const userRemovedFromChannelErr = new Error('user was removed from channel');
export const userLeftChannelErr = new Error('user has left channel');
export const hostRemovedErr = new Error('user was removed by host');

View file

@ -3,21 +3,28 @@
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {Platform, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {hostLowerHand, hostMake, hostMuteSession, hostStopScreenshare} from '@calls/actions/calls';
import {removeFromCall} from '@calls/alerts';
import {useHostMenus} from '@calls/hooks';
import SlideUpPanelItem from '@components/slide_up_panel_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import BottomSheet from '@screens/bottom_sheet';
import {dismissBottomSheet} from '@screens/navigation';
import UserProfileTitle from '@screens/user_profile/title';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
import type {CallSession, CurrentCall} from '@calls/types/calls';
const TITLE_HEIGHT = 118;
const ITEM_HEIGHT = 48;
const SEPARATOR_HEIGHT = 17;
const ANDROID_BUMP_HEIGHT = 20;
type Props = {
currentCall: CurrentCall;
@ -27,11 +34,18 @@ type Props = {
hideGuestTags: boolean;
}
const styles = StyleSheet.create({
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({
iconStyle: {
marginRight: 6,
},
});
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1,
width: '100%',
alignSelf: 'center',
marginVertical: 8,
},
}));
export const HostControls = ({
currentCall,
@ -41,7 +55,10 @@ export const HostControls = ({
hideGuestTags,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const {bottom} = useSafeAreaInsets();
const {openUserProfile} = useHostMenus();
const styles = getStyleFromTheme(theme);
const sharingScreen = currentCall.screenOn === session.sessionId;
@ -63,20 +80,35 @@ export const HostControls = ({
const stopScreensharePress = useCallback(async () => {
hostStopScreenshare(currentCall.serverUrl, currentCall.channelId, session.sessionId);
await dismissBottomSheet();
}, [currentCall.serverUrl, currentCall.id, session.sessionId]);
}, [currentCall.serverUrl, currentCall.channelId, session.sessionId]);
const profilePress = useCallback(async () => {
await dismissBottomSheet();
openUserProfile(session);
}, [session]);
const removePress = useCallback(async () => {
const displayName = displayUsername(session.userModel, intl.locale, teammateNameDisplay, true);
removeFromCall(currentCall.serverUrl, displayName, currentCall.channelId, session.sessionId, intl);
}, [session.userModel, intl, teammateNameDisplay, currentCall.serverUrl, currentCall.channelId, session.sessionId]);
const snapPoints = useMemo(() => {
const items = 1 + (session.muted ? 0 : 1) + (sharingScreen ? 1 : 0) + (session.raisedHand ? 1 : 0);
const items = 3 + (session.muted ? 0 : 1) + (sharingScreen ? 1 : 0) + (session.raisedHand ? 1 : 0);
return [
1,
bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + TITLE_HEIGHT,
bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + TITLE_HEIGHT + SEPARATOR_HEIGHT + (Platform.OS === 'android' ? ANDROID_BUMP_HEIGHT : 0),
];
}, [bottom, session.muted, sharingScreen, session.raisedHand]);
const makeHostText = intl.formatMessage({id: 'mobile.calls_make_host', defaultMessage: 'Make host'});
const muteText = intl.formatMessage({id: 'mobile.calls_mute_participant', defaultMessage: 'Mute participant'});
const lowerHandText = intl.formatMessage({id: 'mobile.calls_lower_hand', defaultMessage: 'Lower hand'});
const stopScreenshareText = intl.formatMessage({id: 'mobile.calls_stop_screenshare', defaultMessage: 'Stop screen share'});
const stopScreenshareText = intl.formatMessage({
id: 'mobile.calls_stop_screenshare',
defaultMessage: 'Stop screen share',
});
const profileText = intl.formatMessage({id: 'mobile.calls_view_profile', defaultMessage: 'View profile'});
const removeText = intl.formatMessage({id: 'mobile.calls_remove_participant', defaultMessage: 'Remove from call'});
const renderContent = () => {
if (!session?.userModel) {
@ -125,6 +157,20 @@ export const HostControls = ({
onPress={makeHostPress}
text={makeHostText}
/>
<SlideUpPanelItem
leftIcon={'account-outline'}
leftIconStyles={styles.iconStyle}
onPress={profilePress}
text={profileText}
/>
<View style={styles.separator}/>
<SlideUpPanelItem
leftIcon={'minus-circle-outline'}
leftIconStyles={styles.iconStyle}
onPress={removePress}
text={removeText}
destructive={true}
/>
</>
);
};

View file

@ -515,6 +515,12 @@
"mobile.calls_recording_start_no_permissions": "You don't have permissions to start a recording. Please ask the call host to start a recording.",
"mobile.calls_recording_stop_no_permissions": "You don't have permissions to stop the recording. Please ask the call host to stop the recording.",
"mobile.calls_recording_stop_none_in_progress": "No recording is in progress.",
"mobile.calls_remove": "Remove",
"mobile.calls_remove_alert_body": "Are you sure you want to remove {displayName} from the call? ",
"mobile.calls_remove_alert_title": "Remove participant",
"mobile.calls_remove_participant": "Remove from call",
"mobile.calls_removed_alert_body": "The host removed you from the call.",
"mobile.calls_removed_alert_title": "You were removed from the call",
"mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance",
"mobile.calls_request_title": "Calls is not currently enabled",
"mobile.calls_see_logs": "See server logs",
@ -531,6 +537,7 @@
"mobile.calls_user_left_channel_error_title": "You left the channel",
"mobile.calls_user_removed_from_channel_error_message": "You have been removed from the channel, and have been disconnected from the call.",
"mobile.calls_user_removed_from_channel_error_title": "You were removed from channel",
"mobile.calls_view_profile": "View profile",
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
"mobile.calls_you": "(you)",
"mobile.calls_you_2": "You",