init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import React, { useCallback, useState } from 'react';
import { GestureResponderEvent, Text, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import styles from '../../../breakout-rooms/components/native/styles';
interface IProps {
/**
* The children to be displayed within this list.
*/
children: React.ReactNode;
/**
* Callback to invoke when the {@code CollapsibleList} is long pressed.
*/
onLongPress?: (e?: GestureResponderEvent) => void;
/**
* Collapsible list title.
*/
title: Object;
}
const CollapsibleList = ({ children, onLongPress, title }: IProps) => {
const [ collapsed, setCollapsed ] = useState(false);
const _toggleCollapsed = useCallback(() => {
setCollapsed(!collapsed);
}, [ collapsed ]);
return (
<View>
<TouchableOpacity
onLongPress = { onLongPress }
onPress = { _toggleCollapsed }
style = { styles.collapsibleList as ViewStyle }>
<TouchableOpacity
onPress = { _toggleCollapsed }
style = { styles.arrowIcon as ViewStyle }>
<Icon
size = { 18 }
src = { collapsed ? IconArrowDown : IconArrowUp } />
</TouchableOpacity>
<Text style = { styles.listTile as TextStyle }>
{ title }
</Text>
</TouchableOpacity>
{ !collapsed && children }
</View>
);
};
export default CollapsibleList;

View File

@@ -0,0 +1,67 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import Avatar from '../../../base/avatar/components/Avatar';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
import { IParticipant } from '../../../base/participants/types';
import { setKnockingParticipantApproval } from '../../../lobby/actions.native';
import { getKnockingParticipantsById } from '../../../lobby/functions';
import styles from './styles';
interface IProps {
/**
* Participant reference.
*/
participant: IParticipant;
}
const ContextMenuLobbyParticipantReject = ({ participant: p }: IProps) => {
const dispatch = useDispatch();
const knockParticipantsIDArr = useSelector(getKnockingParticipantsById);
const knockParticipantIsAvailable = knockParticipantsIDArr.find(knockPartId => knockPartId === p.id);
const displayName = p.name;
const reject = useCallback(() => {
dispatch(setKnockingParticipantApproval(p.id, false));
},
[ dispatch ]);
const { t } = useTranslation();
// eslint-disable-next-line react/no-multi-comp
const renderMenuHeader = () => (
<View
style = { styles.contextMenuItemSectionAvatar as ViewStyle }>
<Avatar
participantId = { p.id }
size = { 24 } />
<Text style = { styles.contextMenuItemName }>
{ displayName }
</Text>
</View>
);
return (
<BottomSheet
addScrollViewPadding = { false }
/* eslint-disable-next-line react/jsx-no-bind */
renderHeader = { renderMenuHeader }
showSlidingView = { Boolean(knockParticipantIsAvailable) }>
<TouchableOpacity
onPress = { reject }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.reject') }</Text>
</TouchableOpacity>
</BottomSheet>
);
};
export default ContextMenuLobbyParticipantReject;

View File

@@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View, ViewStyle } from 'react-native';
import { Divider, Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
requestDisableAudioModeration,
requestDisableVideoModeration,
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../av-moderation/constants';
import {
isEnabled as isAvModerationEnabled,
isSupported as isAvModerationSupported
} from '../../../av-moderation/functions';
import { getCurrentConference } from '../../../base/conference/functions';
import { hideSheet, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
import { raiseHand } from '../../../base/participants/actions';
import { getRaiseHandsQueue, isLocalParticipantModerator } from '../../../base/participants/functions';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
import MuteEveryonesVideoDialog
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
import styles from './styles';
export const ContextMenuMore = () => {
const dispatch = useDispatch();
const muteAllVideo = useCallback(() => {
dispatch(openDialog(MuteEveryonesVideoDialog));
dispatch(hideSheet());
}, [ dispatch ]);
const conference = useSelector(getCurrentConference);
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
const moderator = useSelector(isLocalParticipantModerator);
const lowerAllHands = useCallback(() => {
dispatch(raiseHand(false));
conference?.sendEndpointMessage('', { name: LOWER_HAND_MESSAGE });
dispatch(hideSheet());
}, [ dispatch ]);
const { t } = useTranslation();
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
return (
<BottomSheet
addScrollViewPadding = { false }
showSlidingView = { true }>
<TouchableOpacity
onPress = { muteAllVideo }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconVideoOff } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
</TouchableOpacity>
{ moderator && raisedHandsQueue.length !== 0 && <TouchableOpacity
onPress = { lowerAllHands }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconRaiseHand } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.lowerAllHands')}</Text>
</TouchableOpacity> }
{isModerationSupported && <>
{/* @ts-ignore */}
<Divider style = { styles.divider } />
<View style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.allow')}</Text>
</View>
{isAudioModerationEnabled
? <TouchableOpacity
onPress = { disableAudioModeration }
style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.audioModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableAudioModeration }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCheck } />
<Text style = { styles.contextMenuItemText }>
{t('participantsPane.actions.audioModeration')}
</Text>
</TouchableOpacity> }
{isVideoModerationEnabled
? <TouchableOpacity
onPress = { disableVideoModeration }
style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.videoModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableVideoModeration }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCheck } />
<Text style = { styles.contextMenuItemText }>
{t('participantsPane.actions.videoModeration')}
</Text>
</TouchableOpacity>}
</>}
</BottomSheet>
);
};

View File

@@ -0,0 +1,45 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { IParticipant } from '../../../base/participants/types';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { setKnockingParticipantApproval } from '../../../lobby/actions.native';
import ParticipantItem from './ParticipantItem';
import styles from './styles';
interface IProps {
/**
* Participant reference.
*/
participant: IParticipant;
}
export const LobbyParticipantItem = ({ participant: p }: IProps) => {
const dispatch = useDispatch();
const admit = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, true)), [ dispatch, p.id ]);
const reject = useCallback(() => dispatch(setKnockingParticipantApproval(p.id, false)), [ dispatch, p.id ]);
return (
<ParticipantItem
displayName = { p.name ?? '' }
isKnockingParticipant = { true }
key = { p.id }
participantID = { p.id } >
<Button
accessibilityLabel = 'participantsPane.actions.reject'
labelKey = 'participantsPane.actions.reject'
onClick = { reject }
style = { styles.buttonReject }
type = { BUTTON_TYPES.DESTRUCTIVE } />
<Button
accessibilityLabel = 'participantsPane.actions.admit'
labelKey = 'participantsPane.actions.admit'
onClick = { admit }
style = { styles.buttonAdmit }
type = { BUTTON_TYPES.PRIMARY } />
</ParticipantItem>
);
};

View File

@@ -0,0 +1,57 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
import { admitMultiple } from '../../../lobby/actions.native';
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
import { LobbyParticipantItem } from './LobbyParticipantItem';
import styles from './styles';
const LobbyParticipantList = () => {
const dispatch = useDispatch();
const lobbyEnabled = useSelector(getLobbyEnabled);
const participants = useSelector(getKnockingParticipants);
const admitAll = useCallback(() =>
dispatch(admitMultiple(participants)),
[ dispatch, participants ]);
const { t } = useTranslation();
const title = t('participantsPane.headings.waitingLobby',
{ count: participants.length });
if (!lobbyEnabled || !participants.length) {
return null;
}
return (
<>
<View style = { styles.listDetails as ViewStyle } >
<Text style = { styles.lobbyListDescription as TextStyle }>
{ title }
</Text>
{
participants.length > 1 && (
<Button
accessibilityLabel = 'participantsPane.actions.admitAll'
labelKey = 'participantsPane.actions.admitAll'
mode = { BUTTON_MODES.TEXT }
onClick = { admitAll }
type = { BUTTON_TYPES.PRIMARY } />
)
}
</View>
{
participants.map(p => (
<LobbyParticipantItem
key = { p.id }
participant = { p } />)
)
}
</>
);
};
export default LobbyParticipantList;

View File

@@ -0,0 +1,195 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName,
hasRaisedHand,
isParticipantModerator
} from '../../../base/participants/functions';
import { FakeParticipant, IParticipant } from '../../../base/participants/types';
import {
isParticipantAudioMuted,
isParticipantVideoMuted
} from '../../../base/tracks/functions.native';
import { showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
import type { MediaState } from '../../constants';
import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
import ParticipantItem from './ParticipantItem';
interface IProps {
/**
* Media state for audio.
*/
_audioMediaState: MediaState;
/**
* Whether or not to disable the moderator indicator.
*/
_disableModeratorIndicator?: boolean;
/**
* The display name of the participant.
*/
_displayName: string;
/**
* The type of fake participant.
*/
_fakeParticipant: FakeParticipant;
/**
* Whether or not the user is a moderator.
*/
_isModerator: boolean;
/**
* True if the participant is the local participant.
*/
_local: boolean;
/**
* Shared video local participant owner.
*/
_localVideoOwner: boolean;
/**
* The participant ID.
*/
_participantID: string;
/**
* True if the participant have raised hand.
*/
_raisedHand: boolean;
/**
* Media state for video.
*/
_videoMediaState: MediaState;
/**
* The redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The participant.
*/
participant?: IParticipant;
}
/**
* Implements the MeetingParticipantItem component.
*/
class MeetingParticipantItem extends PureComponent<IProps> {
/**
* Creates new MeetingParticipantItem instance.
*
* @param {IProps} props - The props of the component.
*/
constructor(props: IProps) {
super(props);
this._onPress = this._onPress.bind(this);
}
/**
* Handles MeetingParticipantItem press events.
*
* @returns {void}
*/
_onPress() {
const {
_fakeParticipant,
_local,
_localVideoOwner,
_participantID,
dispatch
} = this.props;
if (_fakeParticipant && _localVideoOwner) {
dispatch(showSharedVideoMenu(_participantID));
} else if (!_fakeParticipant) {
dispatch(showContextMenuDetails(_participantID, _local));
} // else no-op
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
_audioMediaState,
_disableModeratorIndicator,
_displayName,
_isModerator,
_local,
_participantID,
_raisedHand,
_videoMediaState
} = this.props;
return (
<ParticipantItem
audioMediaState = { _audioMediaState }
disableModeratorIndicator = { _disableModeratorIndicator }
displayName = { _displayName }
isModerator = { _isModerator }
local = { _local }
onPress = { this._onPress }
participantID = { _participantID }
raisedHand = { _raisedHand }
videoMediaState = { _videoMediaState } />
);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participant } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
const { disableModeratorIndicator } = state['features/base/config'];
const raisedHand = hasRaisedHand(participant?.local
? participant
: getParticipantById(state, participant?.id)
);
return {
_audioMediaState: audioMediaState,
_disableModeratorIndicator: disableModeratorIndicator,
_displayName: getParticipantDisplayName(state, participant?.id),
_fakeParticipant: participant?.fakeParticipant,
_isAudioMuted,
_isModerator: isParticipantModerator(participant),
_local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_participantID: participant?.id,
_raisedHand: raisedHand,
_videoMediaState: videoMediaState
};
}
export default translate(connect(mapStateToProps)(MeetingParticipantItem));

View File

@@ -0,0 +1,121 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, Text, TextStyle, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconAddUser } from '../../../base/icons/svg';
import {
addPeopleFeatureControl,
getLocalParticipant,
getParticipantCountWithFake,
getRemoteParticipants,
setShareDialogVisiblity
} from '../../../base/participants/functions';
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 {
getBreakoutRooms,
getCurrentRoomId
} from '../../../breakout-rooms/functions';
import { doInvitePeople } from '../../../invite/actions.native';
import { getInviteOthersControl } from '../../../share-room/functions';
import { iAmVisitor } from '../../../visitors/functions';
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
import MeetingParticipantItem from './MeetingParticipantItem';
import styles from './styles';
const MeetingParticipantList = () => {
const currentRoomId = useSelector(getCurrentRoomId);
const currentRoom = useSelector(getBreakoutRooms)[currentRoomId];
const dispatch = useDispatch();
const inviteOthersControl = useSelector(getInviteOthersControl);
const isAddPeopleFeatureEnabled = useSelector(addPeopleFeatureControl);
const keyExtractor
= useCallback((e: undefined, i: number) => i.toString(), []);
const localParticipant = useSelector(getLocalParticipant);
const _iAmVisitor = useSelector(iAmVisitor);
const onInvite = useCallback(() => {
setShareDialogVisiblity(isAddPeopleFeatureEnabled, dispatch);
dispatch(doInvitePeople());
}, [ dispatch ]);
const [ searchString, setSearchString ] = useState('');
const onSearchStringChange = useCallback((text: string) =>
setSearchString(text), []);
const participantsCount = useSelector(getParticipantCountWithFake);
const remoteParticipants = useSelector(getRemoteParticipants);
const renderParticipant = ({ item/* , index, separators */ }: any) => {
const participant = item === localParticipant?.id
? localParticipant : remoteParticipants.get(item);
if (participantMatchesSearch(participant, searchString)) {
return (
<MeetingParticipantItem
key = { item }
participant = { participant } />
);
}
return null;
};
const showInviteButton = useSelector(shouldRenderInviteButton);
const sortedRemoteParticipants = useSelector(
(state: IReduxState) => state['features/filmstrip'].remoteParticipants);
const { t } = useTranslation();
const title = currentRoom?.name
? `${currentRoom.name} (${participantsCount})`
: t('participantsPane.headings.participantsList',
{ count: participantsCount });
const { color, shareDialogVisible } = inviteOthersControl;
return (
<View style = { styles.meetingListContainer }>
<Text
style = { styles.meetingListDescription as TextStyle }>
{ title }
</Text>
{
showInviteButton
&& <Button
accessibilityLabel = 'participantsPane.actions.invite'
disabled = { shareDialogVisible }
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
icon = { () => (
<Icon
color = { color }
size = { 20 }
src = { IconAddUser } />
) }
labelKey = 'participantsPane.actions.invite'
onClick = { onInvite }
style = { styles.inviteButton }
type = { BUTTON_TYPES.PRIMARY } />
}
<Input
clearable = { true }
customStyles = {{
container: styles.inputContainer,
input: styles.centerInput }}
onChange = { onSearchStringChange }
placeholder = { t('participantsPane.search') }
value = { searchString } />
<FlatList
data = { _iAmVisitor
? [ ...sortedRemoteParticipants ]
: [ localParticipant?.id, ...sortedRemoteParticipants ] as Array<any>
}
keyExtractor = { keyExtractor }
/* eslint-disable react/jsx-no-bind */
renderItem = { renderParticipant }
windowSize = { 2 } />
</View>
);
};
export default MeetingParticipantList;

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
GestureResponderEvent,
StyleProp,
TextStyle,
TouchableOpacity,
View,
ViewStyle
} from 'react-native';
import { Text } from 'react-native-paper';
import Avatar from '../../../base/avatar/components/Avatar';
import { AudioStateIcons, MEDIA_STATE, type MediaState, VideoStateIcons } from '../../constants';
import { RaisedHandIndicator } from './RaisedHandIndicator';
import styles from './styles';
interface IProps {
/**
* Media state for audio.
*/
audioMediaState?: MediaState;
/**
* React children.
*/
children?: React.ReactNode;
/**
* Whether or not to disable the moderator indicator.
*/
disableModeratorIndicator?: boolean;
/**
* The name of the participant. Used for showing lobby names.
*/
displayName: string;
/**
* Is the participant waiting?
*/
isKnockingParticipant?: boolean;
/**
* Whether or not the user is a moderator.
*/
isModerator?: boolean;
/**
* True if the participant is local.
*/
local?: boolean;
/**
* Callback to be invoked on pressing the participant item.
*/
onPress?: (e?: GestureResponderEvent) => void;
/**
* The ID of the participant.
*/
participantID: string;
/**
* True if the participant have raised hand.
*/
raisedHand?: boolean;
/**
* Media state for video.
*/
videoMediaState?: MediaState;
}
/**
* Participant item.
*
* @returns {React$Element<any>}
*/
function ParticipantItem({
children,
displayName,
disableModeratorIndicator,
isKnockingParticipant = false,
isModerator,
local,
onPress,
participantID,
raisedHand,
audioMediaState = MEDIA_STATE.NONE,
videoMediaState = MEDIA_STATE.NONE
}: IProps) {
const { t } = useTranslation();
const participantNameContainerStyles
= isKnockingParticipant ? styles.lobbyParticipantNameContainer : styles.participantNameContainer;
return (
<View style = { styles.participantContainer as StyleProp<ViewStyle> } >
<TouchableOpacity
onPress = { onPress }
style = { styles.participantContent as StyleProp<ViewStyle> }>
<Avatar
displayName = { displayName }
participantId = { participantID }
size = { 32 } />
<View
style = { [
styles.participantDetailsContainer,
raisedHand && styles.participantDetailsContainerRaisedHand
] as StyleProp<ViewStyle> }>
<View style = { participantNameContainerStyles as StyleProp<ViewStyle> }>
<Text
numberOfLines = { 1 }
style = { styles.participantName as StyleProp<TextStyle> }>
{ displayName }
{ local && ` (${t('chat.you')})` }
</Text>
</View>
{
isModerator && !disableModeratorIndicator
&& <Text style = { styles.moderatorLabel as StyleProp<TextStyle> }>
{ t('videothumbnail.moderator') }
</Text>
}
</View>
{
!isKnockingParticipant
&& <>
{ raisedHand && <RaisedHandIndicator /> }
<View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
<View>{ AudioStateIcons[audioMediaState] }</View>
</View>
</>
}
</TouchableOpacity>
{ !local && children }
</View>
);
}
export default ParticipantItem;

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Text } from 'react-native';
import { useSelector } from 'react-redux';
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
import styles from './styles';
const ParticipantsCounter = () => {
const participantsCount = useSelector(getParticipantCountForDisplay);
return <Text style = { styles.participantsBadge }>{participantsCount}</Text>;
};
export default ParticipantsCounter;

View File

@@ -0,0 +1,50 @@
import React, { useCallback } from 'react';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import LobbyParticipantList from './LobbyParticipantList';
import MeetingParticipantList from './MeetingParticipantList';
import ParticipantsPaneFooter from './ParticipantsPaneFooter';
import VisitorsList from './VisitorsList';
import styles from './styles';
/**
* Participants pane.
*
* @returns {React$Element<any>}
*/
const ParticipantsPane = () => {
const isLocalModerator = useSelector(isLocalParticipantModerator);
const keyExtractor
= useCallback((e: undefined, i: number) => i.toString(), []);
const renderListHeaderComponent = () => (
<>
<VisitorsList />
<LobbyParticipantList />
<MeetingParticipantList />
</>
);
return (
<JitsiScreen
footerComponent = { isLocalModerator ? ParticipantsPaneFooter : undefined }
style = { styles.participantsPaneContainer }>
{ /* Fixes warning regarding nested lists */ }
<FlatList
// eslint-disable-next-line react/jsx-no-bind
ListHeaderComponent = { renderListHeaderComponent }
data = { [] as ReadonlyArray<undefined> }
keyExtractor = { keyExtractor }
renderItem = { null }
windowSize = { 2 } />
</JitsiScreen>
);
};
export default ParticipantsPane;

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconUsers } from '../../../base/icons/svg';
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import ParticipantsCounter from './ParticipantsConter';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link ParticipantsPaneButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Participants count.
*/
_participantsCount: number;
}
/**
* Implements an {@link AbstractButton} to open the participants panel.
*/
class ParticipantsPaneButton extends AbstractButton<IProps> {
override icon = IconUsers;
override label = 'toolbar.participants';
/**
* Handles clicking / pressing the button, and opens the participants panel.
*
* @private
* @returns {void}
*/
override _handleClick() {
return navigate(screen.conference.participants);
}
/**
* Override the _getAccessibilityLabel method to incorporate the dynamic participant count.
*
* @override
* @returns {string}
*/
_getAccessibilityLabel() {
const { t, _participantsCount } = this.props;
return t('toolbar.accessibilityLabel.participants', {
participantsCount: _participantsCount
});
}
/**
* Overrides AbstractButton's {@link Component#render()}.
*
* @override
* @protected
* @returns {React.ReactElement}
*/
override render() {
return (
<View style = { styles.participantsButtonBadge as ViewStyle }>
{ super.render() }
<ParticipantsCounter />
</View>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
return {
_participantsCount: getParticipantCountForDisplay(state)
};
}
export default translate(connect(mapStateToProps)(ParticipantsPaneButton));

View File

@@ -0,0 +1,95 @@
/* eslint-disable lines-around-comment */
import React, { useCallback } from 'react';
import { View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openDialog, openSheet } from '../../../base/dialog/actions';
import {
BREAKOUT_ROOMS_BUTTON_ENABLED
} from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconDotsHorizontal, IconRingGroup } from '../../../base/icons/svg';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
// @ts-ignore
import MuteEveryoneDialog from '../../../video-menu/components/native/MuteEveryoneDialog';
import { isMoreActionsVisible, isMuteAllVisible } from '../../functions';
import { ContextMenuMore } from './ContextMenuMore';
import styles from './styles';
/**
* Implements the participants pane footer component.
*
* @returns { JSX.Element} - The participants pane footer component.
*/
const ParticipantsPaneFooter = (): JSX.Element => {
const dispatch = useDispatch();
const isBreakoutRoomsSupported = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getBreakoutRooms()?.isSupported()
);
const isBreakoutRoomsEnabled = useSelector((state: IReduxState) =>
getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true)
);
const openMoreMenu = useCallback(() => dispatch(openSheet(ContextMenuMore)), [ dispatch ]);
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
[ dispatch ]);
const showMoreActions = useSelector(isMoreActionsVisible);
const showMuteAll = useSelector(isMuteAllVisible);
return (
<View style = { styles.participantsPaneFooterContainer as ViewStyle }>
{
isBreakoutRoomsSupported
&& isBreakoutRoomsEnabled
&& <Button
accessibilityLabel = 'participantsPane.actions.breakoutRooms'
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
icon = { () => (
<Icon
color = { BaseTheme.palette.icon04 }
size = { 20 }
src = { IconRingGroup } />
) }
labelKey = 'participantsPane.actions.breakoutRooms'
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
onClick = { () => navigate(screen.conference.breakoutRooms) }
style = { styles.breakoutRoomsButton }
type = { BUTTON_TYPES.SECONDARY } />
}
<View style = { styles.participantsPaneFooter as ViewStyle }>
{
showMuteAll && (
<Button
accessibilityLabel = 'participantsPane.actions.muteAll'
labelKey = 'participantsPane.actions.muteAll'
onClick = { muteAll }
type = { BUTTON_TYPES.SECONDARY } />
)
}
{
showMoreActions && (
<IconButton
onPress = { openMoreMenu }
src = { IconDotsHorizontal }
style = { styles.moreButton }
type = { BUTTON_TYPES.SECONDARY } />
)
}
</View>
</View>
);
};
export default ParticipantsPaneFooter;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { IconRaiseHand } from '../../../base/icons/svg';
import styles from './styles';
export const RaisedHandIndicator = () => (
<View style = { styles.raisedHandIndicator as StyleProp<ViewStyle> }>
<Icon
size = { 16 }
src = { IconRaiseHand }
style = { styles.raisedHandIcon } />
</View>
);

View File

@@ -0,0 +1,145 @@
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n/functions';
import { getBreakoutRooms } from '../../../breakout-rooms/functions';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import SendToBreakoutRoom from '../../../video-menu/components/native/SendToBreakoutRoom';
import styles from '../../../video-menu/components/native/styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 24;
interface IProps extends WithTranslation {
/**
* The list of all breakout rooms.
*/
_rooms: Array<any>;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The jid of the selected participant.
*/
participantJid: string;
/**
* The display name of the selected participant.
*/
participantName: string;
/**
* The room the participant is in.
*/
room: any;
}
/**
* Class to implement a popup menu that opens upon long pressing a thumbnail.
*/
class RoomParticipantMenu extends PureComponent<IProps> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const { _rooms, participantJid, room, t } = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: participantJid,
styles: bottomSheetStyles.buttons
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { true }>
<View style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemText as ViewStyle }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</Text>
</View>
{_rooms.map(r => room.id !== r.id && (<SendToBreakoutRoom
key = { r.id }
room = { r }
{ ...buttonProps } />))}
</BottomSheet>
);
}
/**
* Callback to hide the {@code RemoteVideoMenu}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantName } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
displayName = { participantName }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel as TextStyle }>
{ participantName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_rooms: Object.values(getBreakoutRooms(state))
};
}
export default translate(connect(_mapStateToProps)(RoomParticipantMenu));

View File

@@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { approveRequest, denyRequest } from '../../../visitors/actions';
import { IPromotionRequest } from '../../../visitors/types';
import ParticipantItem from './ParticipantItem';
import styles from './styles';
interface IProps {
/**
* Promotion request reference.
*/
request: IPromotionRequest;
}
export const VisitorsItem = ({ request: r }: IProps) => {
const dispatch = useDispatch();
const admit = useCallback(() => dispatch(approveRequest(r)), [ dispatch, r ]);
const reject = useCallback(() => dispatch(denyRequest(r)), [ dispatch, r ]);
const { from, nick } = r;
return (
<ParticipantItem
displayName = { nick ?? '' }
isKnockingParticipant = { true }
key = { from }
participantID = { from } >
<Button
accessibilityLabel = 'participantsPane.actions.reject'
labelKey = 'participantsPane.actions.reject'
onClick = { reject }
style = { styles.buttonReject }
type = { BUTTON_TYPES.DESTRUCTIVE } />
<Button
accessibilityLabel = 'participantsPane.actions.admit'
labelKey = 'participantsPane.actions.admit'
onClick = { admit }
style = { styles.buttonAdmit }
type = { BUTTON_TYPES.PRIMARY } />
</ParticipantItem>
);
};

View File

@@ -0,0 +1,90 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
import { admitMultiple, goLive } from '../../../visitors/actions';
import {
getPromotionRequests,
getVisitorsCount,
getVisitorsInQueueCount,
isVisitorsLive
} from '../../../visitors/functions';
import { VisitorsItem } from './VisitorsItem';
import styles from './styles';
const VisitorsList = () => {
const visitorsCount = useSelector(getVisitorsCount);
const dispatch = useDispatch();
const requests = useSelector(getPromotionRequests);
const admitAll = useCallback(() => {
dispatch(admitMultiple(requests));
}, [ dispatch, requests ]);
const goLiveCb = useCallback(() => {
dispatch(goLive());
}, [ dispatch ]);
const { t } = useTranslation();
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
const isLive = useSelector(isVisitorsLive);
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
if (visitorsCount <= 0 && !showVisitorsInQueue) {
return null;
}
let title = t('participantsPane.headings.visitors', { count: visitorsCount });
if (requests.length > 0) {
title += t('participantsPane.headings.visitorRequests', { count: requests.length });
}
if (showVisitorsInQueue) {
title += t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount });
}
return (
<>
<View style = { styles.listDetails as ViewStyle } >
<Text style = { styles.visitorsLabel }>
{ title }
</Text>
{
requests.length > 1 && !showVisitorsInQueue && (
<Button
accessibilityLabel = 'participantsPane.actions.admitAll'
labelKey = 'participantsPane.actions.admitAll'
mode = { BUTTON_MODES.TEXT }
onClick = { admitAll }
type = { BUTTON_TYPES.PRIMARY } />
)
}
{
showVisitorsInQueue && (
<Button
accessibilityLabel = 'participantsPane.actions.goLive'
labelKey = 'participantsPane.actions.goLive'
mode = { BUTTON_MODES.TEXT }
onClick = { goLiveCb }
type = { BUTTON_TYPES.PRIMARY } />
)
}
</View>
{
requests.map(r => (
<VisitorsItem
key = { r.from }
request = { r } />)
)
}
</>
);
};
export default VisitorsList;

View File

@@ -0,0 +1,293 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The style for participant list description.
*/
const participantListDescription = {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.text01,
fontSize: 15,
fontWeight: 'bold',
marginLeft: BaseTheme.spacing[2],
paddingVertical: BaseTheme.spacing[2],
position: 'relative',
width: '70%'
};
/**
* The style for content.
*/
const flexContent = {
alignItems: 'center',
color: BaseTheme.palette.icon01,
display: 'flex',
flex: 1
};
/**
* The style for the context menu items text.
*/
const contextMenuItemText = {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01
};
/**
* The style of the participants pane buttons.
*/
export const button = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center'
};
/**
* The style of the context menu pane items.
*/
const contextMenuItem = {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[7],
marginLeft: BaseTheme.spacing[3]
};
const participantNameContainer = {
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: BaseTheme.spacing[3]
};
/**
* The styles of the native components of the feature {@code participants}.
*/
export default {
participantsBadge: {
backgroundColor: BaseTheme.palette.ui03,
borderRadius: BaseTheme.spacing[2],
borderColor: 'white',
overflow: 'hidden',
height: BaseTheme.spacing[3],
minWidth: BaseTheme.spacing[3],
color: BaseTheme.palette.text01,
...BaseTheme.typography.labelBold,
position: 'absolute',
right: -3,
top: -3,
textAlign: 'center',
paddingHorizontal: 2
},
participantsButtonBadge: {
display: 'flex',
position: 'relative'
},
participantContainer: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[9],
paddingLeft: BaseTheme.spacing[3],
paddingRight: BaseTheme.spacing[3],
width: '100%'
},
participantContent: {
alignItems: 'center',
borderBottomColor: BaseTheme.palette.ui02,
borderBottomWidth: 2.4,
display: 'flex',
flexDirection: 'row',
height: '100%',
overflow: 'hidden',
width: '100%'
},
participantDetailsContainer: {
display: 'flex',
flexDirection: 'column',
width: '73%'
},
participantDetailsContainerRaisedHand: {
width: '65%'
},
participantNameContainer: {
...participantNameContainer,
width: '100%'
},
lobbyParticipantNameContainer: {
...participantNameContainer,
width: '40%'
},
participantName: {
color: BaseTheme.palette.text01,
overflow: 'hidden'
},
moderatorLabel: {
color: BaseTheme.palette.text03,
alignSelf: 'flex-start',
paddingLeft: BaseTheme.spacing[3],
paddingTop: BaseTheme.spacing[1]
},
participantStatesContainer: {
display: 'flex',
flexDirection: 'row',
marginLeft: 'auto',
width: '15%'
},
participantStateVideo: {
paddingRight: BaseTheme.spacing[3]
},
raisedHandIndicator: {
backgroundColor: BaseTheme.palette.warning02,
borderRadius: BaseTheme.shape.borderRadius / 2,
height: BaseTheme.spacing[4],
width: BaseTheme.spacing[4],
marginLeft: 'auto',
marginRight: BaseTheme.spacing[2]
},
raisedHandIcon: {
...flexContent,
top: BaseTheme.spacing[1],
color: BaseTheme.palette.uiBackground
},
buttonAdmit: {
position: 'absolute',
right: 16
},
buttonReject: {
position: 'absolute',
right: 112
},
lobbyListDescription: {
...participantListDescription
},
listDetails: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
},
meetingListContainer: {
paddingHorizontal: BaseTheme.spacing[3]
},
meetingListDescription: {
...participantListDescription
},
participantsPaneContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1,
flexDirection: 'column',
paddingVertical: BaseTheme.spacing[2]
},
participantsPaneFooterContainer: {
alignItems: 'center',
bottom: 0,
height: 128,
left: 0,
paddingHorizontal: BaseTheme.spacing[4],
right: 0
},
participantsPaneFooter: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingBottom: BaseTheme.spacing[3],
width: '100%'
},
inviteButton: {
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3],
marginVertical: BaseTheme.spacing[2]
},
breakoutRoomsButton: {
marginBottom: BaseTheme.spacing[2],
width: '100%'
},
moreButton: {
marginLeft: BaseTheme.spacing[2]
},
contextMenuItem: {
...contextMenuItem
},
contextMenuItemSection: {
...contextMenuItem
},
contextMenuItemSectionAvatar: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01,
borderBottomColor: BaseTheme.palette.ui07,
borderBottomWidth: 1,
borderTopLeftRadius: BaseTheme.spacing[3],
borderTopRightRadius: BaseTheme.spacing[3],
flexDirection: 'row',
height: BaseTheme.spacing[7],
paddingLeft: BaseTheme.spacing[3]
},
contextMenuItemText: {
...contextMenuItemText,
marginLeft: BaseTheme.spacing[3]
},
contextMenuItemTextNoIcon: {
...contextMenuItemText,
marginLeft: BaseTheme.spacing[6]
},
contextMenuItemName: {
color: BaseTheme.palette.text04,
flexShrink: 1,
fontSize: BaseTheme.spacing[3],
marginLeft: BaseTheme.spacing[3],
opacity: 0.90
},
divider: {
backgroundColor: BaseTheme.palette.ui07
},
inputContainer: {
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3],
marginBottom: BaseTheme.spacing[4]
},
centerInput: {
paddingRight: BaseTheme.spacing[3],
textAlign: 'center'
},
visitorsLabel: {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.warning02,
marginLeft: BaseTheme.spacing[2]
}
};