This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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));
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
293
react/features/participants-pane/components/native/styles.ts
Normal file
293
react/features/participants-pane/components/native/styles.ts
Normal 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]
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user