snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

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', 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.meetingListContainer }>
<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 } />)
)
}
</View>
);
};
export default LobbyParticipantList;

View File

@@ -0,0 +1,170 @@
import React, { useCallback } from 'react';
import { connect, useDispatch } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName,
hasRaisedHand,
isParticipantModerator
} from '../../../base/participants/functions';
import { FakeParticipant } 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,
participantMatchesSearch
} 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;
/**
* Whether or not the participant name matches the search string.
*/
_matchesSearch: boolean;
/**
* True if the participant have raised hand.
*/
_raisedHand: boolean;
/**
* Media state for video.
*/
_videoMediaState: MediaState;
/**
* The participant ID.
*/
participantID: string;
/**
* Name of the participant we search for.
*/
searchString: string;
}
const MeetingParticipantItem = ({
_audioMediaState,
_disableModeratorIndicator,
_displayName,
_fakeParticipant,
_isModerator,
_local,
_localVideoOwner,
_matchesSearch,
_raisedHand,
_videoMediaState,
participantID
}: IProps) => {
const dispatch = useDispatch();
const onPress = useCallback(() => {
if (_fakeParticipant && _localVideoOwner) {
dispatch(showSharedVideoMenu(participantID));
} else if (!_fakeParticipant) {
dispatch(showContextMenuDetails(participantID, _local));
} // else no-op
}, [ dispatch ]);
if (!_matchesSearch) {
return null;
}
return (
<ParticipantItem
audioMediaState = { _audioMediaState }
disableModeratorIndicator = { _disableModeratorIndicator }
displayName = { _displayName }
isModerator = { _isModerator }
local = { _local }
onPress = { 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 { participantID, searchString } = ownProps;
const { ownerId } = state['features/shared-video'];
const participant = getParticipantById(state, participantID);
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, participantID)
);
const _matchesSearch = participantMatchesSearch(participant, searchString);
return {
_audioMediaState: audioMediaState,
_disableModeratorIndicator: disableModeratorIndicator,
_displayName: getParticipantDisplayName(state, participantID),
_fakeParticipant: participant?.fakeParticipant,
_isAudioMuted,
_isModerator: isParticipantModerator(participant),
_local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_matchesSearch,
_raisedHand: raisedHand,
_videoMediaState: videoMediaState
};
}
// @ts-ignore
export default connect(mapStateToProps)(MeetingParticipantItem);

View File

@@ -0,0 +1,174 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, Text, TextStyle, View } from 'react-native';
import { connect, useDispatch } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconAddUser } from '../../../base/icons/svg';
import {
addPeopleFeatureControl,
getParticipantById,
isScreenShareParticipant,
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 { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
import MeetingParticipantItem from './MeetingParticipantItem';
import styles from './styles';
interface IProps {
currentRoom?: {
jid: string;
name: string;
};
iconColor: string;
isAddPeopleFeatureEnabled?: boolean | undefined;
isShareDialogVisible: boolean;
participantsCount?: number;
showInviteButton?: boolean;
sortedParticipantIds?: Array<string>;
visitorsCount?: number | undefined;
}
const MeetingParticipantList = ({
currentRoom,
iconColor,
isAddPeopleFeatureEnabled,
isShareDialogVisible,
participantsCount,
showInviteButton,
sortedParticipantIds = [],
visitorsCount
}: IProps): any => {
const { t } = useTranslation();
const [ searchString, setSearchString ] = useState('');
const dispatch = useDispatch();
const keyExtractor = useCallback((e: undefined, i: number) => i.toString(), []);
const onInvite = useCallback(() => {
setShareDialogVisiblity(isAddPeopleFeatureEnabled, dispatch);
dispatch(doInvitePeople());
}, [ dispatch ]);
const onSearchStringChange = useCallback((text: string) =>
setSearchString(text), []);
const title = currentRoom?.name
? `${currentRoom.name} (${participantsCount})`
: t('participantsPane.headings.participantsList',
{ count: participantsCount });
const visitorsLabelText = visitorsCount && visitorsCount > 0
? t('participantsPane.headings.visitors', { count: visitorsCount })
: undefined;
const renderParticipant = ({ item }: any) => (
<MeetingParticipantItem
key = { item }
participantID = { item }
searchString = { searchString } />
);
return (
<View style = { styles.meetingListContainer }>
<Text style = { styles.visitorsLabel as TextStyle }>
{ visitorsLabelText }
</Text>
<Text
style = { styles.meetingListDescription as TextStyle }>
{ title }
</Text>
{
showInviteButton
&& <Button
accessibilityLabel = 'participantsPane.actions.invite'
disabled = { isShareDialogVisible }
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
icon = { () => (
<Icon
color = { iconColor }
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 = { sortedParticipantIds as Array<any> }
keyExtractor = { keyExtractor }
/* eslint-disable react/jsx-no-bind */
renderItem = { renderParticipant }
windowSize = { 2 } />
</View>
);
};
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
let sortedParticipantIds: any = getSortedParticipantIds(state);
const _iAmVisitor = iAmVisitor(state);
sortedParticipantIds = sortedParticipantIds.filter((id: any) => {
const participant = getParticipantById(state, id);
if (_iAmVisitor && participant?.local) {
return false;
}
return !isScreenShareParticipant(participant);
});
const currentRoomId = getCurrentRoomId(state);
const currentRoom = getBreakoutRooms(state)[currentRoomId];
const inviteOthersControl = getInviteOthersControl(state);
const { color, shareDialogVisible } = inviteOthersControl;
const isAddPeopleFeatureEnabled = addPeopleFeatureControl(state);
const participantsCount = sortedParticipantIds.length;
const showInviteButton = shouldRenderInviteButton(state);
const visitorsCount = state['features/visitors']?.count || 0;
return {
currentRoom,
iconColor: color,
inviteOthersControl,
isAddPeopleFeatureEnabled,
isShareDialogVisible: shareDialogVisible,
participantsCount,
showInviteButton,
sortedParticipantIds,
visitorsCount
};
}
export default connect(_mapStateToProps)(MeetingParticipantList);

View File

@@ -0,0 +1,137 @@
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();
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>
<View style = { styles.participantNameContainer 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
&& <View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
<View>{ raisedHand && <RaisedHandIndicator /> }</View>
<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,45 @@
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';
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', 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,262 @@
import { Platform } from 'react-native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The style for participant list description.
*/
const participantListDescription = {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.text01,
marginLeft: BaseTheme.spacing[2],
paddingVertical: BaseTheme.spacing[2]
};
/**
* 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]
};
/**
* 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]
},
participantContent: {
alignItems: 'center',
borderBottomColor: BaseTheme.palette.ui02,
borderBottomWidth: 2.4,
display: 'flex',
flexDirection: 'row',
height: '100%',
overflow: 'hidden',
width: '100%'
},
participantNameContainer: {
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: BaseTheme.spacing[3],
},
participantName: {
color: BaseTheme.palette.text01,
overflow: 'hidden'
},
moderatorLabel: {
color: BaseTheme.palette.text03,
alignSelf: 'flex-start',
paddingLeft: BaseTheme.spacing[3],
paddingTop: BaseTheme.spacing[1]
},
participantStatesContainer: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
marginLeft: 'auto'
},
participantStateVideo: {
marginRight: BaseTheme.spacing[2]
},
raisedHandIndicator: {
backgroundColor: BaseTheme.palette.warning02,
borderRadius: BaseTheme.shape.borderRadius,
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: BaseTheme.spacing[3]
},
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: {
alignSelf: 'stretch',
marginHorizontal: BaseTheme.spacing[5]
},
participantsPaneFooter: {
display: 'flex',
flexDirection: 'row',
marginLeft: 'auto'
},
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: {
marginHorizontal: BaseTheme.spacing[3],
marginBottom: BaseTheme.spacing[3]
},
centerInput: {
...Platform.select({ ios: { textAlign: 'center' } })
},
visitorsLabel: {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.warning02,
marginLeft: BaseTheme.spacing[2]
}
};