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

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

View File

@@ -0,0 +1,32 @@
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 { createBreakoutRoom } from '../../actions';
import styles from './styles';
/**
* Button to add a breakout room.
*
* @returns {JSX.Element} - The add breakout room button.
*/
const AddBreakoutRoomButton = () => {
const dispatch = useDispatch();
const onAdd = useCallback(() =>
dispatch(createBreakoutRoom())
, [ dispatch ]);
return (
<Button
accessibilityLabel = 'breakoutRooms.actions.add'
labelKey = 'breakoutRooms.actions.add'
onClick = { onAdd }
style = { styles.button }
type = { BUTTON_TYPES.SECONDARY } />
);
};
export default AddBreakoutRoomButton;

View File

@@ -0,0 +1,33 @@
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 { autoAssignToBreakoutRooms } from '../../actions';
import styles from './styles';
/**
* Button to auto assign participants to breakout rooms.
*
* @returns {JSX.Element} - The auto assign button.
*/
const AutoAssignButton = () => {
const dispatch = useDispatch();
const onAutoAssign = useCallback(() => {
dispatch(autoAssignToBreakoutRooms());
}, [ dispatch ]);
return (
<Button
accessibilityLabel = 'breakoutRooms.actions.autoAssign'
labelKey = 'breakoutRooms.actions.autoAssign'
labelStyle = { styles.autoAssignLabel }
onClick = { onAutoAssign }
style = { styles.autoAssignButton }
type = { BUTTON_TYPES.TERTIARY } />
);
};
export default AutoAssignButton;

View File

@@ -0,0 +1,125 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { hideSheet, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge, IconEdit, IconRingGroup } from '../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import styles from '../../../participants-pane/components/native/styles';
import { isBreakoutRoomRenameAllowed } from '../../../participants-pane/functions';
import { BREAKOUT_CONTEXT_MENU_ACTIONS as ACTIONS } from '../../../participants-pane/types';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../actions';
import { getBreakoutRoomsConfig } from '../../functions';
import { IRoom } from '../../types';
import BreakoutRoomNamePrompt from './BreakoutRoomNamePrompt';
/**
* An array with all possible breakout rooms actions.
*/
const ALL_ACTIONS = [ ACTIONS.JOIN, ACTIONS.REMOVE, ACTIONS.RENAME ];
interface IProps {
/**
* The actions that will be displayed.
*/
actions: Array<ACTIONS>;
/**
* The room for which the menu is open.
*/
room: IRoom;
}
const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
const dispatch = useDispatch();
const isLocalModerator = useSelector(isLocalParticipantModerator);
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
const _isBreakoutRoomRenameAllowed = useSelector(isBreakoutRoomRenameAllowed);
const { t } = useTranslation();
const onJoinRoom = useCallback(() => {
sendAnalytics(createBreakoutRoomsEvent('join'));
dispatch(moveToRoom(room.jid));
dispatch(hideSheet());
}, [ dispatch, room ]);
const onRemoveBreakoutRoom = useCallback(() => {
dispatch(removeBreakoutRoom(room.jid));
dispatch(hideSheet());
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
breakoutRoomJid: room.jid,
initialRoomName: room.name
}));
dispatch(hideSheet());
}, [ dispatch, room ]);
const onCloseBreakoutRoom = useCallback(() => {
dispatch(closeBreakoutRoom(room.id));
dispatch(hideSheet());
}, [ dispatch, room ]);
return (
<BottomSheet
addScrollViewPadding = { false }
showSlidingView = { true }>
{
!hideJoinRoomButton && actions.includes(ACTIONS.JOIN) && (
<TouchableOpacity
onPress = { onJoinRoom }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconRingGroup } />
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.join')}</Text>
</TouchableOpacity>
)
}
{
!room?.isMainRoom && actions.includes(ACTIONS.RENAME) && _isBreakoutRoomRenameAllowed
&& <TouchableOpacity
onPress = { onRenameBreakoutRoom }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconEdit } />
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.rename')}</Text>
</TouchableOpacity>
}
{
!room?.isMainRoom && isLocalModerator && actions.includes(ACTIONS.REMOVE)
&& (room?.participants && Object.keys(room.participants).length > 0
? <TouchableOpacity
onPress = { onCloseBreakoutRoom }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.close')}</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { onRemoveBreakoutRoom }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconCloseLarge } />
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.remove')}</Text>
</TouchableOpacity>
)
}
</BottomSheet>
);
};
export default BreakoutRoomContextMenu;

View File

@@ -0,0 +1,35 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import InputDialog from '../../../base/dialog/components/native/InputDialog';
import { IBreakoutRoomNamePromptProps as IProps } from '../../../participants-pane/types';
import { renameBreakoutRoom } from '../../actions';
/**
* Implements a component to render a breakout room name prompt.
*
* @param {IProps} props - The props of the component.
* @returns {JSX.Element}
*/
export default function BreakoutRoomNamePrompt({ breakoutRoomJid, initialRoomName }: IProps) {
const dispatch = useDispatch();
const onSubmit = useCallback((roomName: string) => {
const formattedRoomName = roomName?.trim();
if (formattedRoomName) {
dispatch(renameBreakoutRoom(breakoutRoomJid, formattedRoomName));
return true;
}
return false;
}, [ breakoutRoomJid, dispatch ]);
return (
<InputDialog
descriptionKey = 'dialog.renameBreakoutRoomTitle'
initialValue = { initialRoomName?.trim() }
onSubmit = { onSubmit } />
);
}

View File

@@ -0,0 +1,43 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isLocalParticipantModerator, isParticipantModerator } from '../../../base/participants/functions';
import { showRoomParticipantMenu } from '../../../participants-pane/actions.native';
import ParticipantItem from '../../../participants-pane/components/native/ParticipantItem';
import { IRoom } from '../../types';
interface IProps {
/**
* Participant to be displayed.
*/
item: any;
/**
* The room the participant is in.
*/
room: IRoom;
}
const BreakoutRoomParticipantItem = ({ item, room }: IProps) => {
const { defaultRemoteDisplayName = '' } = useSelector((state: IReduxState) => state['features/base/config']);
const moderator = useSelector(isLocalParticipantModerator);
const dispatch = useDispatch();
const onPress = useCallback(() => {
if (moderator) {
dispatch(showRoomParticipantMenu(room, item.jid, item.displayName));
}
}, [ moderator, room, item ]);
return (
<ParticipantItem
displayName = { item.displayName || defaultRemoteDisplayName }
isModerator = { isParticipantModerator(item) }
key = { item.jid }
onPress = { onPress }
participantID = { item.jid } />
);
};
export default BreakoutRoomParticipantItem;

View File

@@ -0,0 +1,67 @@
import React, { useCallback, useMemo } from 'react';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { equals } from '../../../base/redux/functions';
import {
getBreakoutRooms,
getCurrentRoomId,
isAddBreakoutRoomButtonVisible,
isAutoAssignParticipantsVisible,
isInBreakoutRoom
} from '../../functions';
import AddBreakoutRoomButton from './AddBreakoutRoomButton';
import AutoAssignButton from './AutoAssignButton';
import { CollapsibleRoom } from './CollapsibleRoom';
import LeaveBreakoutRoomButton from './LeaveBreakoutRoomButton';
import styles from './styles';
const BreakoutRooms = () => {
const currentRoomId = useSelector(getCurrentRoomId);
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isBreakoutRoomsSupported = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
const isLocalModerator = useSelector(isLocalParticipantModerator);
const keyExtractor = useCallback((e: undefined, i: number) => i.toString(), []);
const rooms = Object.values(useSelector(getBreakoutRooms, equals))
.filter(room => room.id !== currentRoomId)
.sort((p1, p2) => (p1?.name || '').localeCompare(p2?.name || ''));
const showAddBreakoutRoom = useSelector(isAddBreakoutRoomButtonVisible);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
const renderListHeaderComponent = useMemo(() => (
<>
{ showAutoAssign && <AutoAssignButton /> }
{ inBreakoutRoom && <LeaveBreakoutRoomButton /> }
{
isBreakoutRoomsSupported
&& rooms.map(room => (<CollapsibleRoom
key = { room.id }
room = { room }
roomId = { room.id } />))
}
</>
), [ showAutoAssign, inBreakoutRoom, isBreakoutRoomsSupported, rooms ]);
return (
<JitsiScreen
footerComponent = { isLocalModerator && showAddBreakoutRoom
? AddBreakoutRoomButton : undefined }
style = { styles.breakoutRoomsContainer }>
{ /* Fixes warning regarding nested lists */ }
<FlatList
ListHeaderComponent = { renderListHeaderComponent }
data = { [] as ReadonlyArray<undefined> }
keyExtractor = { keyExtractor }
renderItem = { null }
windowSize = { 2 } />
</JitsiScreen>
);
};
export default BreakoutRooms;

View File

@@ -0,0 +1,53 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
BREAKOUT_ROOMS_BUTTON_ENABLED
} from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconRingGroup } from '../../../base/icons/svg';
import AbstractButton,
{
IProps as AbstractButtonProps
} from '../../../base/toolbox/components/AbstractButton';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the breakout room screen.
*/
class BreakoutRoomsButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.breakoutRooms';
override icon = IconRingGroup;
override label = 'breakoutRooms.buttonLabel';
/**
* Handles clicking / pressing the button and opens the breakout rooms screen.
*
* @private
* @returns {void}
*/
override _handleClick() {
return navigate(screen.conference.breakoutRooms);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {IReduxState} state - The Redux state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true);
return {
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(BreakoutRoomsButton));

View File

@@ -0,0 +1,66 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList } from 'react-native';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../base/dialog/actions';
import CollapsibleList from '../../../participants-pane/components/native/CollapsibleList';
import { IRoom } from '../../types';
import BreakoutRoomContextMenu from './BreakoutRoomContextMenu';
import BreakoutRoomParticipantItem from './BreakoutRoomParticipantItem';
interface IProps {
/**
* Room to display.
*/
room: IRoom;
roomId: string;
}
/**
* Returns a key for a passed item of the list.
*
* @param {Object} item - The participant.
* @returns {string} - The user ID.
*/
function _keyExtractor(item: any) {
return item.jid;
}
export const CollapsibleRoom = ({ room, roomId }: IProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const _openContextMenu = useCallback(() => {
dispatch(openSheet(BreakoutRoomContextMenu, { room }));
}, [ room ]);
const roomParticipantsNr = Object.values(room.participants || {}).length;
const title
= `${room.name
|| t('breakoutRooms.mainRoom')} (${roomParticipantsNr})`;
return (
<CollapsibleList
onLongPress = { _openContextMenu }
title = { title }>
<FlatList
data = { Object.values(room.participants || {}) }
keyExtractor = { _keyExtractor }
/* @ts-ignore */
listKey = { roomId as String }
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
renderItem = { ({ item: participant }) => (
<BreakoutRoomParticipantItem
item = { participant }
room = { room } />
) }
scrollEnabled = { false }
showsHorizontalScrollIndicator = { false }
windowSize = { 2 } />
</CollapsibleList>
);
};

View File

@@ -0,0 +1,36 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { moveToRoom } from '../../actions';
import styles from './styles';
/**
* Button to leave a breakout rooms.
*
* @returns {JSX.Element} - The leave breakout room button.
*/
const LeaveBreakoutRoomButton = () => {
const dispatch = useDispatch();
const onLeave = useCallback(() => {
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}
, [ dispatch ]);
return (
<Button
accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
onClick = { onLeave }
style = { styles.button }
type = { BUTTON_TYPES.DESTRUCTIVE } />
);
};
export default LeaveBreakoutRoomButton;

View File

@@ -0,0 +1,76 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The styles of the native components of the feature {@code breakout rooms}.
*/
export default {
button: {
marginBottom: BaseTheme.spacing[4],
marginHorizontal: BaseTheme.spacing[2]
},
collapsibleList: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[7],
marginHorizontal: BaseTheme.spacing[2],
marginTop: BaseTheme.spacing[3]
},
arrowIcon: {
backgroundColor: BaseTheme.palette.ui03,
height: BaseTheme.spacing[5],
width: BaseTheme.spacing[5],
borderRadius: 6,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
roomName: {
fontSize: 15,
color: BaseTheme.palette.text01,
fontWeight: 'bold',
marginLeft: BaseTheme.spacing[2]
},
listTile: {
fontSize: 15,
color: BaseTheme.palette.text01,
fontWeight: 'bold',
marginLeft: BaseTheme.spacing[2]
},
autoAssignLabel: {
color: BaseTheme.palette.link01
},
autoAssignButton: {
alignSelf: 'center',
justifyContent: 'center',
marginTop: BaseTheme.spacing[3]
},
breakoutRoomsContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1,
flexDirection: 'column',
height: 'auto',
paddingHorizontal: BaseTheme.spacing[3]
},
inputContainer: {
marginLeft: BaseTheme.spacing[2],
marginRight: BaseTheme.spacing[2],
marginTop: BaseTheme.spacing[4]
},
centerInput: {
paddingRight: BaseTheme.spacing[3],
textAlign: 'center'
}
};