Files
jitsi-meet/react/features/prejoin/components/native/Prejoin.tsx
Calinteodor 63e69c224d feat(prejoin): fix join meeting from external/calendar link while in another meeting (#15310)
If you are in meeting and you want to join another meeting from an external link, it is not possible.
That is because Prejoin screen didn't get unmounted, isJoining remains true and button is unpressable.
Most probably because react navigation, where the screen only gets focused or blurred.
2024-11-18 16:15:03 +02:00

228 lines
9.4 KiB
TypeScript

import { useIsFocused } from '@react-navigation/native';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
BackHandler,
Platform,
StyleProp,
Text,
TextStyle,
View,
ViewStyle
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { setPermanentProperty } from '../../../analytics/actions';
import { appNavigate } from '../../../app/actions.native';
import { IReduxState } from '../../../app/types';
import { setAudioOnly } from '../../../base/audio-only/actions';
import { getConferenceName } from '../../../base/conference/functions';
import { isNameReadOnly } from '../../../base/config/functions.any';
import { connect } from '../../../base/connection/actions.native';
import { PREJOIN_PAGE_HIDE_DISPLAY_NAME } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { IconCloseLarge } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getFieldValue } from '../../../base/react/functions';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { updateSettings } from '../../../base/settings/actions';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { openDisplayNamePrompt } from '../../../display-name/actions';
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
import LargeVideo from '../../../large-video/components/LargeVideo.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions';
import { IPrejoinProps } from '../../types';
import { hasDisplayName } from '../../utils';
import { preJoinStyles as styles } from './styles';
const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
const dispatch = useDispatch();
const isFocused = useIsFocused();
const { t } = useTranslation();
const aspectRatio = useSelector(
(state: IReduxState) => state['features/base/responsive-ui']?.aspectRatio
);
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
const isDisplayNameMandatory = useSelector((state: IReduxState) => isDisplayNameRequired(state));
const isDisplayNameVisible
= useSelector((state: IReduxState) => !getFeatureFlag(state, PREJOIN_PAGE_HIDE_DISPLAY_NAME, false));
const isDisplayNameReadonly = useSelector(isNameReadOnly);
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
const participantName = localParticipant?.name;
const [ displayName, setDisplayName ]
= useState(participantName || '');
const isDisplayNameMissing = useMemo(
() => !displayName && isDisplayNameMandatory, [ displayName, isDisplayNameMandatory ]);
const showDisplayNameError = useMemo(
() => !isDisplayNameReadonly && isDisplayNameMissing && isDisplayNameVisible,
[ isDisplayNameMissing, isDisplayNameReadonly, isDisplayNameVisible ]);
const showDisplayNameInput = useMemo(
() => isDisplayNameVisible && (displayName || !isDisplayNameReadonly),
[ displayName, isDisplayNameReadonly, isDisplayNameVisible ]);
const onChangeDisplayName = useCallback(event => {
const fieldValue = getFieldValue(event);
setDisplayName(fieldValue);
dispatch(updateSettings({
displayName: fieldValue
}));
}, [ displayName ]);
const onJoin = useCallback(() => {
dispatch(connect());
navigateRoot(screen.conference.root);
}, [ dispatch ]);
const maybeJoin = useCallback(() => {
if (isDisplayNameMissing) {
dispatch(openDisplayNamePrompt({
onPostSubmit: onJoin,
validateInput: hasDisplayName
}));
} else {
onJoin();
}
}, [ dispatch, hasDisplayName, isDisplayNameMissing, onJoin ]);
const onJoinLowBandwidth = useCallback(() => {
dispatch(setAudioOnly(true));
maybeJoin();
}, [ dispatch ]);
const goBack = useCallback(() => {
dispatch(appNavigate(undefined));
return true;
}, [ dispatch ]);
const { PRIMARY, TERTIARY } = BUTTON_TYPES;
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', goBack);
dispatch(setPermanentProperty({
wasPrejoinDisplayed: true
}));
return () => BackHandler.removeEventListener('hardwareBackPress', goBack);
}, []); // dispatch is not in the dependancy list because we want the action to be dispatched only once when
// the component is mounted.
const headerLeft = () => {
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconCloseLarge } />
);
};
useLayoutEffect(() => {
navigation.setOptions({
headerLeft,
headerTitle: t('prejoin.joinMeeting')
});
}, [ navigation ]);
let contentWrapperStyles;
let contentContainerStyles;
let largeVideoContainerStyles;
if (aspectRatio === ASPECT_RATIO_NARROW) {
contentWrapperStyles = styles.contentWrapper;
contentContainerStyles = styles.contentContainer;
largeVideoContainerStyles = styles.largeVideoContainer;
} else {
contentWrapperStyles = styles.contentWrapperWide;
contentContainerStyles = styles.contentContainerWide;
largeVideoContainerStyles = styles.largeVideoContainerWide;
}
return (
<JitsiScreen
addBottomPadding = { false }
safeAreaInsets = { [ 'right' ] }
style = { contentWrapperStyles }>
<BrandingImageBackground />
{
isFocused
&& <View style = { largeVideoContainerStyles as StyleProp<ViewStyle> }>
<View style = { styles.conferenceInfo as StyleProp<ViewStyle> }>
{roomNameEnabled && (
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
<Text
numberOfLines = { 1 }
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
{ roomName }
</Text>
</View>
)}
</View>
<LargeVideo />
</View>
}
<View style = { contentContainerStyles as ViewStyle }>
<View style = { styles.toolboxContainer as ViewStyle }>
<AudioMuteButton
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
</View>
{
showDisplayNameInput && <Input
customStyles = {{ input: styles.customInput }}
disabled = { isDisplayNameReadonly }
error = { showDisplayNameError }
onChange = { onChangeDisplayName }
placeholder = { t('dialog.enterDisplayName') }
value = { displayName } />
}
{
showDisplayNameError && (
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
<Text style = { styles.error as StyleProp<TextStyle> }>
{ t('prejoin.errorMissingName') }
</Text>
</View>
)
}
<Button
accessibilityLabel = 'prejoin.joinMeeting'
disabled = { showDisplayNameError }
labelKey = 'prejoin.joinMeeting'
onClick = { maybeJoin }
style = { styles.joinButton }
type = { PRIMARY } />
<Button
accessibilityLabel = 'prejoin.joinMeetingInLowBandwidthMode'
disabled = { showDisplayNameError }
labelKey = 'prejoin.joinMeetingInLowBandwidthMode'
onClick = { onJoinLowBandwidth }
style = { styles.joinButton }
type = { TERTIARY } />
</View>
</JitsiScreen>
);
};
export default Prejoin;