{
+ return {
+ clearableInput: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ height: '20px',
+ border: `1px solid ${theme.palette.border02}`,
+ backgroundColor: theme.palette.uiBackground,
+ position: 'relative',
+ borderRadius: '6px',
+ padding: '10px 16px',
+
+ '&.focused': {
+ border: `3px solid ${theme.palette.field01Focus}`
+ }
+ },
+ clearButton: {
+ backgroundColor: 'transparent',
+ border: 0,
+ position: 'absolute',
+ right: '10px',
+ top: '11px',
+ padding: 0,
+
+ '& svg': {
+ fill: theme.palette.icon02
+ }
+ },
+ input: {
+ backgroundColor: 'transparent',
+ border: 0,
+ width: '100%',
+ height: '100%',
+ borderRadius: '6px',
+ fontSize: '14px',
+ lineHeight: '20px',
+ textAlign: 'center',
+ caretColor: theme.palette.text01,
+ color: theme.palette.text01,
+
+ '&::placeholder': {
+ color: theme.palette.text03
+ }
+ }
+ };
+});
+
+/**
+ * Implements a pre-styled clearable input field.
+ *
+ * @param {Props} props - The props of the component.
+ * @returns {ReactElement}
+ */
+function ClearableInput({
+ autoFocus = false,
+ autoComplete,
+ className = '',
+ id,
+ onChange,
+ onSubmit,
+ placeholder,
+ testId,
+ type = 'text',
+ value
+}: Props) {
+ const classes = useStyles();
+ const [ val, setVal ] = useState(value || '');
+ const [ focused, setFocused ] = useState(false);
+ const inputRef = React.createRef();
+
+ useEffect(() => {
+ if (value && value !== val) {
+ setVal(value);
+ }
+ }, [ value ]);
+
+
+ /**
+ * Callback for the onBlur event of the field.
+ *
+ * @returns {void}
+ */
+ const _onBlur = useCallback(() => {
+ setFocused(false);
+ });
+
+ /**
+ * Callback for the onChange event of the field.
+ *
+ * @param {Object} evt - The static event.
+ * @returns {void}
+ */
+ const _onChange = useCallback(evt => {
+ const newValue = evt.target.value;
+
+ setVal(newValue);
+ onChange && onChange(newValue);
+ }, [ onChange ]);
+
+ /**
+ * Callback for the onFocus event of the field.
+ *
+ * @returns {void}
+ */
+ const _onFocus = useCallback(() => {
+ setFocused(true);
+ });
+
+ /**
+ * Joins the conference on 'Enter'.
+ *
+ * @param {Event} event - Key down event object.
+ * @returns {void}
+ */
+ const _onKeyDown = useCallback(event => {
+ onSubmit && event.key === 'Enter' && onSubmit();
+ }, [ onSubmit ]);
+
+ /**
+ * Clears the input.
+ *
+ * @returns {void}
+ */
+ const _clearInput = useCallback(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ setVal('');
+ onChange && onChange('');
+ }, [ onChange ]);
+
+ return (
+
+
+ {val !== '' && (
+
+ )}
+
+ );
+}
+
+export default ClearableInput;
diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.js b/react/features/participants-pane/components/web/MeetingParticipantItem.js
index d0077f0bb..ca52858bb 100644
--- a/react/features/participants-pane/components/web/MeetingParticipantItem.js
+++ b/react/features/participants-pane/components/web/MeetingParticipantItem.js
@@ -19,6 +19,7 @@ import {
isParticipantAudioMuted,
isParticipantVideoMuted
} from '../../../base/tracks';
+import { normalizeAccents } from '../../../base/util/strings';
import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
import {
getParticipantAudioMediaState,
@@ -72,6 +73,11 @@ type Props = {
*/
_localVideoOwner: boolean,
+ /**
+ * Whether or not the participant name matches the search string.
+ */
+ _matchesSearch: boolean,
+
/**
* The participant.
*/
@@ -175,6 +181,7 @@ function MeetingParticipantItem({
_displayName,
_local,
_localVideoOwner,
+ _matchesSearch,
_participant,
_participantID,
_quickActionButtonType,
@@ -222,6 +229,10 @@ function MeetingParticipantItem({
};
}, [ _audioTrack ]);
+ if (!_matchesSearch) {
+ return null;
+ }
+
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
@@ -280,12 +291,31 @@ function MeetingParticipantItem({
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
- const { participantID } = ownProps;
+ const { participantID, searchString } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const participant = getParticipantByIdOrUndefined(state, participantID);
+ const _displayName = getParticipantDisplayName(state, participant?.id);
+
+ let _matchesSearch = false;
+ const names = normalizeAccents(_displayName)
+ .toLowerCase()
+ .split(' ');
+ const lowerCaseSearchString = searchString.toLowerCase();
+
+ if (lowerCaseSearchString === '') {
+ _matchesSearch = true;
+ } else {
+ for (const name of names) {
+ if (name.startsWith(lowerCaseSearchString)) {
+ _matchesSearch = true;
+ break;
+ }
+ }
+ }
+
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
@@ -302,9 +332,10 @@ function _mapStateToProps(state, ownProps): Object {
_audioMediaState,
_audioTrack,
_disableModeratorIndicator: disableModeratorIndicator,
- _displayName: getParticipantDisplayName(state, participant?.id),
+ _displayName,
_local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
+ _matchesSearch,
_participant: participant,
_participantID: participant?.id,
_quickActionButtonType,
diff --git a/react/features/participants-pane/components/web/MeetingParticipantItems.js b/react/features/participants-pane/components/web/MeetingParticipantItems.js
index 19cb3fa2c..afebbb0b2 100644
--- a/react/features/participants-pane/components/web/MeetingParticipantItems.js
+++ b/react/features/participants-pane/components/web/MeetingParticipantItems.js
@@ -56,6 +56,11 @@ type Props = {
*/
participantActionEllipsisLabel: string,
+ /**
+ * Current search string.
+ */
+ searchString?: string,
+
/**
* The translated "you" text.
*/
@@ -78,8 +83,9 @@ function MeetingParticipantItems({
overflowDrawer,
raiseContextId,
participantActionEllipsisLabel,
+ searchString,
youText
-}) {
+}: Props) {
const renderParticipant = id => (
);
diff --git a/react/features/participants-pane/components/web/MeetingParticipants.js b/react/features/participants-pane/components/web/MeetingParticipants.js
index 765fc5dad..c660227bf 100644
--- a/react/features/participants-pane/components/web/MeetingParticipants.js
+++ b/react/features/participants-pane/components/web/MeetingParticipants.js
@@ -11,11 +11,13 @@ import {
getParticipantCountWithFake
} from '../../../base/participants';
import { connect } from '../../../base/redux';
+import { normalizeAccents } from '../../../base/util/strings';
import { showOverflowDrawer } from '../../../toolbox/functions';
import { muteRemote } from '../../../video-menu/actions.any';
import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
import { useParticipantDrawer } from '../../hooks';
+import ClearableInput from './ClearableInput';
import { InviteButton } from './InviteButton';
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
import MeetingParticipantItems from './MeetingParticipantItems';
@@ -56,6 +58,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
const isMouseOverMenu = useRef(false);
const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
+ const [ searchString, setSearchString ] = useState('');
const { t } = useTranslation();
const lowerMenu = useCallback(() => {
@@ -123,6 +126,9 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
<>
{t('participantsPane.headings.participantsList', { count: participantsCount })}
{showInviteButton && }
+