feat(participants-pane) Added search in participants list (#9975)

- created `ClearableInpu`t component on web & native
- added `ClearableInput` component to participants pane and used it for search in participants list
- update `AddPeopleDialog` to use `ClearableInput`
This commit is contained in:
robertpin 2021-10-21 14:58:44 +03:00 committed by GitHub
parent da5603dd9a
commit 338ff43c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 653 additions and 117 deletions

View File

@ -627,7 +627,8 @@
"stopVideo": "Stop video",
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
"videoModeration": "Start their video"
}
},
"search": "Search participants"
},
"passwordSetRemotely": "Set by another participant",
"passwordDigitsOnly": "Up to {{number}} digits",

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666748 9.00001C0.666748 13.6024 4.39771 17.3333 9.00008 17.3333C13.6025 17.3333 17.3334 13.6024 17.3334 9.00001C17.3334 4.39763 13.6025 0.666672 9.00008 0.666672C4.39771 0.666672 0.666748 4.39763 0.666748 9.00001ZM12.5356 5.46447C12.2102 5.13903 11.6825 5.13903 11.3571 5.46447L9.00008 7.82149L6.64306 5.46447C6.31762 5.13903 5.78998 5.13903 5.46455 5.46447C5.13911 5.78991 5.13911 6.31755 5.46455 6.64298L7.82157 9L5.46455 11.357C5.13911 11.6825 5.13911 12.2101 5.46455 12.5355C5.78998 12.861 6.31762 12.861 6.64306 12.5355L9.00008 10.1785L11.3571 12.5355C11.6825 12.861 12.2102 12.861 12.5356 12.5355C12.8611 12.2101 12.8611 11.6825 12.5356 11.357L10.1786 9L12.5356 6.64298C12.8611 6.31755 12.8611 5.78991 12.5356 5.46447Z"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@ -29,6 +29,7 @@ export { default as IconCheck } from './check.svg';
export { default as IconCheckSolid } from './check-solid.svg';
export { default as IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseSolid } from './close-solid.svg';
export { default as IconCloseX } from './close-x.svg';
export { default as IconClosedCaption } from './closed_caption.svg';
export { default as IconCloseSmall } from './close-small.svg';

View File

@ -12,3 +12,14 @@ import * as unorm from 'unorm';
export function normalizeNFKC(text: string) {
return unorm.nfkc(text);
}
/**
* Replaces accent characters with english alphabet characters.
* NOTE: Here we use the unorm package because the JSC version in React Native for Android crashes.
*
* @param {string} text - The text that needs to be normalized.
* @returns {string} - The normalized text.
*/
export function normalizeAccents(text: string) {
return unorm.nfd(text).replace(/[\u0300-\u036f]/g, '');
}

View File

@ -9,3 +9,14 @@
export function normalizeNFKC(text: string) {
return text.normalize('NFKC');
}
/**
* Replaces accent characters with english alphabet characters.
* NOTE: Here we use the unorm package because the JSC version in React Native for Android crashes.
*
* @param {string} text - The text that needs to be normalized.
* @returns {string} - The normalized text.
*/
export function normalizeAccents(text: string) {
return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

View File

@ -5,8 +5,6 @@ import React from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
TextInput,
TouchableOpacity,
View
} from 'react-native';
@ -18,7 +16,6 @@ import {
Icon,
IconCancelSelection,
IconCheck,
IconClose,
IconPhone,
IconSearch,
IconShare
@ -29,6 +26,7 @@ import {
type Item
} from '../../../../base/react';
import { connect } from '../../../../base/redux';
import ClearableInput from '../../../../participants-pane/components/native/ClearableInput';
import { beginShareRoom } from '../../../../share-room';
import { INVITE_TYPES } from '../../../constants';
import AbstractAddPeopleDialog, {
@ -106,11 +104,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
selectableItems: []
};
/**
* Ref of the search field.
*/
inputFieldRef: ?TextInput;
/**
* TimeoutID to delay the search for the time the user is probably typing.
*/
@ -136,7 +129,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
this._onShareMeeting = this._onShareMeeting.bind(this);
this._onTypeQuery = this._onTypeQuery.bind(this);
this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
this._setFieldRef = this._setFieldRef.bind(this);
}
/**
@ -220,9 +212,18 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
footerComponent = { this._renderShareMeetingButton }
hasTabNavigator = { false }
style = { styles.addPeopleContainer }>
<View
style = { styles.searchFieldWrapper }>
<View style = { styles.searchIconWrapper }>
<ClearableInput
autoFocus = { false }
customStyles = {{
wrapper: styles.searchFieldWrapper,
input: styles.searchField,
clearButton: styles.clearButton,
clearIcon: styles.clearIcon
}}
onChange = { this._onTypeQuery }
placeholder = { this.props.t(`inviteDialog.${placeholderKey}`) }
placeholderColor = { palette.text04 }
prefixComponent = { <View style = { styles.searchIconWrapper }>
{this.state.searchInprogress
? <ActivityIndicator
color = { DARK_GREY }
@ -230,23 +231,8 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
: <Icon
src = { IconSearch }
style = { styles.searchIcon } />}
</View>
<TextInput
autoCorrect = { false }
autoFocus = { false }
onBlur = { this._onFocused(false) }
onChangeText = { this._onTypeQuery }
onFocus = { this._onFocused(true) }
placeholder = {
this.props.t(`inviteDialog.${placeholderKey}`)
}
placeholderTextColor = { palette.text04 }
ref = { this._setFieldRef }
spellCheck = { false }
style = { styles.searchField }
</View> }
value = { this.state.fieldValue } />
{ this._renderClearButton() }
</View>
{ Boolean(inviteItems.length) && <View style = { styles.invitedList }>
<FlatList
data = { inviteItems }
@ -337,22 +323,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
this._onTypeQuery('');
}
_onFocused: boolean => Function;
/**
* Constructs a callback to be used to update the padding of the field if necessary.
*
* @param {boolean} focused - True of the field is focused.
* @returns {Function}
*/
_onFocused(focused) {
return () => {
Platform.OS === 'android' && this.setState({
bottomPadding: focused
});
};
}
_onInvite: () => void
/**
@ -458,37 +428,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
.finally(() => {
this.setState({
searchInprogress: false
}, () => {
this.inputFieldRef && this.inputFieldRef.focus();
});
});
}
_query: (string) => Promise<Array<Object>>;
/**
* Renders a button to clear the text field.
*
* @returns {React#Element<*>}
*/
_renderClearButton() {
if (!this.state.fieldValue.length) {
return null;
}
return (
<TouchableOpacity
onPress = { this._onClearField }
style = { styles.clearButton }>
<View style = { styles.clearIconContainer }>
<Icon
src = { IconClose }
style = { styles.clearIcon } />
</View>
</TouchableOpacity>
);
}
_renderInvitedItem: Object => React$Element<any> | null
/**
@ -619,18 +564,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
);
}
_setFieldRef: ?TextInput => void
/**
* Sets a reference to the input field for later use.
*
* @param {?TextInput} input - The reference to the input field.
* @returns {void}
*/
_setFieldRef(input) {
this.inputFieldRef = input;
}
/**
* Shows an alert telling the user that some invitees were failed to be
* invited.

View File

@ -31,13 +31,11 @@ export default {
},
clearButton: {
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5
paddingTop: 7
},
clearIcon: {
color: DARK_GREY,
color: BaseTheme.palette.ui02,
fontSize: 18,
textAlign: 'center'
},
@ -100,7 +98,9 @@ export default {
color: DARK_GREY,
flex: 1,
fontSize: 17,
paddingVertical: 7
paddingVertical: 7,
paddingLeft: 0,
textAlign: 'left'
},
selectedIcon: {
@ -117,11 +117,15 @@ export default {
},
searchFieldWrapper: {
backgroundColor: BaseTheme.palette.section01,
alignItems: 'stretch',
flexDirection: 'row',
height: 52,
paddingHorizontal: 12,
paddingVertical: 8
height: 36,
marginHorizontal: 15,
marginVertical: 8,
borderWidth: 0,
borderRadius: 10,
overflow: 'hidden'
},
searchIcon: {
@ -132,8 +136,6 @@ export default {
searchIconWrapper: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.section01,
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
flexDirection: 'row',
justifyContent: 'center',
width: ICON_SIZE + 16

View File

@ -0,0 +1,195 @@
// @flow
import React, { useCallback, useEffect, useState } from 'react';
import { View, TextInput, TouchableOpacity } from 'react-native';
import { withTheme } from 'react-native-paper';
import { Icon, IconCloseSolid } from '../../../base/icons';
import styles from './styles';
type Props = {
/**
* If the input should be focused on display.
*/
autoFocus?: boolean,
/**
* Custom styles for the component.
*/
customStyles?: Object,
/**
* Callback for the onBlur event of the field.
*/
onBlur?: Function,
/**
* Callback for the onChange event of the field.
*/
onChange: Function,
/**
* Callback for the onFocus event of the field.
*/
onFocus?: Function,
/**
* Callback to be used when the user hits Enter in the field.
*/
onSubmit?: Function,
/**
* Placeholder text for the field.
*/
placeholder: string,
/**
* Placeholder text color.
*/
placeholderColor?: string,
/**
* Component to be added to the beginning of the the input.
*/
prefixComponent?: React$Node,
/**
* The type of the return key.
*/
returnKeyType?: 'done' | 'go' | 'next' | 'search' | 'send' | 'none' | 'previous' | 'default',
/**
* Color of the caret and selection.
*/
selectionColor?: string,
/**
* Theme used for styles.
*/
theme: Object,
/**
* Externally provided value.
*/
value?: string
};
/**
* Implements a pre-styled clearable input field.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function ClearableInput({
autoFocus = false,
customStyles = {},
onBlur,
onChange,
onFocus,
onSubmit,
placeholder,
placeholderColor,
prefixComponent,
returnKeyType = 'search',
selectionColor,
theme,
value
}: Props) {
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);
onBlur && onBlur();
}, [ onBlur ]);
/**
* Callback for the onChange event of the field.
*
* @param {Object} evt - The static event.
* @returns {void}
*/
const _onChange = useCallback(evt => {
const { nativeEvent: { text } } = evt;
setVal(text);
onChange && onChange(text);
}, [ onChange ]);
/**
* Callback for the onFocus event of the field.
*
* @returns {void}
*/
const _onFocus = useCallback(() => {
setFocused(true);
onFocus && onFocus();
}, [ onFocus ]);
/**
* Clears the input.
*
* @returns {void}
*/
const _clearInput = useCallback(() => {
if (inputRef.current) {
inputRef.current.focus();
}
setVal('');
onChange && onChange('');
}, [ onChange ]);
return (
<View
style = { [
styles.clearableInput,
focused ? styles.clearableInputFocus : {},
customStyles?.wrapper
] }>
{prefixComponent}
<TextInput
autoCorrect = { false }
autoFocus = { autoFocus }
onBlur = { _onBlur }
onChange = { _onChange }
onFocus = { _onFocus }
onSubmitEditing = { onSubmit }
placeholder = { placeholder }
placeholderTextColor = { placeholderColor ?? theme.palette.text01 }
ref = { inputRef }
returnKeyType = { returnKeyType }
selectionColor = { selectionColor }
style = { [ styles.clearableInputTextInput, customStyles?.input ] }
value = { val } />
{val !== '' && (
<TouchableOpacity
onPress = { _clearInput }
style = { [ styles.clearButton, customStyles?.clearButton ] }>
<Icon
size = { 22 }
src = { IconCloseSolid }
style = { [ styles.clearIcon, customStyles?.clearIcon ] } />
</TouchableOpacity>
)}
</View>
);
}
export default withTheme(ClearableInput);

View File

@ -5,7 +5,6 @@ import React, { PureComponent } from 'react';
import { translate } from '../../../base/i18n';
import {
getLocalParticipant,
getParticipantByIdOrUndefined,
getParticipantDisplayName,
hasRaisedHand,
isParticipantModerator
@ -80,9 +79,9 @@ type Props = {
dispatch: Function,
/**
* The ID of the participant.
* The participant.
*/
participantID: ?string
participant: ?Object
};
/**
@ -171,10 +170,9 @@ class MeetingParticipantItem extends PureComponent<Props> {
* @returns {Props}
*/
function mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
const { participant } = ownProps;
const { ownerId } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state).id;
const participant = getParticipantByIdOrUndefined(state, participantID);
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);

View File

@ -2,30 +2,38 @@
import React, { PureComponent } from 'react';
import { FlatList, Text, View } from 'react-native';
import { Button } from 'react-native-paper';
import { Button, withTheme } from 'react-native-paper';
import { translate } from '../../../base/i18n';
import { Icon, IconInviteMore } from '../../../base/icons';
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { normalizeAccents } from '../../../base/util/strings';
import { doInvitePeople } from '../../../invite/actions.native';
import { shouldRenderInviteButton } from '../../functions';
import ClearableInput from './ClearableInput';
import MeetingParticipantItem from './MeetingParticipantItem';
import styles from './styles';
type Props = {
/**
* The ID of the local participant.
* The local participant.
*/
_localParticipantId: string,
_localParticipant: Object,
/**
* The number of participants in the conference.
*/
_participantsCount: number,
/**
* The remote participants.
*/
_remoteParticipants: Map<string, Object>,
/**
* Whether or not to show the invite button.
*/
@ -44,13 +52,22 @@ type Props = {
/**
* Translation function.
*/
t: Function
t: Function,
/**
* Theme used for styles.
*/
theme: Object
}
type State = {
searchString: string
};
/**
* The meeting participant list component.
*/
class MeetingParticipantList extends PureComponent<Props> {
class MeetingParticipantList extends PureComponent<Props, State> {
/**
* Creates new MeetingParticipantList instance.
@ -60,9 +77,14 @@ class MeetingParticipantList extends PureComponent<Props> {
constructor(props: Props) {
super(props);
this.state = {
searchString: ''
};
this._keyExtractor = this._keyExtractor.bind(this);
this._onInvite = this._onInvite.bind(this);
this._renderParticipant = this._renderParticipant.bind(this);
this._onSearchStringChange = this._onSearchStringChange.bind(this);
}
_keyExtractor: Function;
@ -111,12 +133,50 @@ class MeetingParticipantList extends PureComponent<Props> {
* @returns {ReactElement}
*/
_renderParticipant({ item/* , index, separators */ }) {
const { _localParticipant, _remoteParticipants } = this.props;
const { searchString } = this.state;
const participant = item === _localParticipant?.id ? _localParticipant : _remoteParticipants.get(item);
const displayName = participant?.name;
if (displayName) {
const names = normalizeAccents(displayName)
.toLowerCase()
.split(' ');
const lowerCaseSearch = normalizeAccents(searchString).toLowerCase();
for (const name of names) {
if (lowerCaseSearch === '' || name.startsWith(lowerCaseSearch)) {
return (
<MeetingParticipantItem
key = { item }
participantID = { item } />
participant = { participant } />
);
}
}
} else if (displayName === '' && searchString === '') {
return (
<MeetingParticipantItem
key = { item }
participant = { participant } />
);
}
return null;
}
_onSearchStringChange: (text: string) => void;
/**
* Handles search string changes.
*
* @param {string} text - New value of the search string.
* @returns {void}
*/
_onSearchStringChange(text: string) {
this.setState({
searchString: text
});
}
/**
* Implements React's {@link Component#render()}.
@ -126,7 +186,7 @@ class MeetingParticipantList extends PureComponent<Props> {
*/
render() {
const {
_localParticipantId,
_localParticipant,
_participantsCount,
_showInviteButton,
_sortedRemoteParticipants,
@ -150,9 +210,13 @@ class MeetingParticipantList extends PureComponent<Props> {
onPress = { this._onInvite }
style = { styles.inviteButton } />
}
<ClearableInput
onChange = { this._onSearchStringChange }
placeholder = { t('participantsPane.search') }
selectionColor = { this.props.theme.palette.text01 } />
<FlatList
bounces = { false }
data = { [ _localParticipantId, ..._sortedRemoteParticipants ] }
data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
horizontal = { false }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderParticipant }
@ -176,13 +240,15 @@ function _mapStateToProps(state): Object {
const _participantsCount = getParticipantCountWithFake(state);
const { remoteParticipants } = state['features/filmstrip'];
const _showInviteButton = shouldRenderInviteButton(state);
const _remoteParticipants = getRemoteParticipants(state);
return {
_participantsCount,
_remoteParticipants,
_showInviteButton,
_sortedRemoteParticipants: remoteParticipants,
_localParticipantId: getLocalParticipant(state)?.id
_localParticipant: getLocalParticipant(state)
};
}
export default translate(connect(_mapStateToProps)(MeetingParticipantList));
export default translate(connect(_mapStateToProps)(withTheme(MeetingParticipantList)));

View File

@ -1,3 +1,4 @@
import { MD_ITEM_HEIGHT } from '../../../base/dialog/components/native/styles';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
@ -320,5 +321,52 @@ export default {
divider: {
backgroundColor: BaseTheme.palette.dividerColor
},
clearableInput: {
display: 'flex',
height: MD_ITEM_HEIGHT,
borderWidth: 1,
borderStyle: 'solid',
borderColor: BaseTheme.palette.border02,
backgroundColor: BaseTheme.palette.uiBackground,
borderRadius: 6,
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3]
},
clearableInputFocus: {
borderWidth: 3,
borderColor: BaseTheme.palette.field01Focus
},
clearButton: {
backgroundColor: 'transparent',
borderWidth: 0,
position: 'absolute',
right: 0,
top: 0,
paddingTop: 12,
paddingLeft: BaseTheme.spacing[2],
width: 40,
height: MD_ITEM_HEIGHT
},
clearIcon: {
color: BaseTheme.palette.icon02
},
clearableInputTextInput: {
backgroundColor: 'transparent',
borderWidth: 0,
height: '100%',
width: '100%',
textAlign: 'center',
color: BaseTheme.palette.text01,
paddingTop: BaseTheme.spacing[2],
paddingBottom: BaseTheme.spacing[2],
paddingLeft: BaseTheme.spacing[3],
paddingRight: BaseTheme.spacing[3],
fontSize: 16
}
};

View File

@ -0,0 +1,222 @@
// @flow
import { makeStyles } from '@material-ui/core';
import React, { useCallback, useEffect, useState } from 'react';
import { Icon, IconCloseSolid } from '../../../base/icons';
type Props = {
/**
* String for html autocomplete attribute.
*/
autoComplete?: string,
/**
* If the input should be focused on display.
*/
autoFocus?: boolean,
/**
* Class name to be appended to the default class list.
*/
className?: string,
/**
* Input id.
*/
id?: string,
/**
* Callback for the onChange event of the field.
*/
onChange: Function,
/**
* Callback to be used when the user hits Enter in the field.
*/
onSubmit?: Function,
/**
* Placeholder text for the field.
*/
placeholder: string,
/**
* The field type (e.g. text, password...etc).
*/
type?: string,
/**
* TestId of the button. Can be used to locate element when testing UI.
*/
testId?: string,
/**
* Externally provided value.
*/
value?: string
};
const useStyles = makeStyles(theme => {
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 (
<div className = { `${classes.clearableInput} ${focused ? 'focused' : ''} ${className || ''}` }>
<input
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { classes.input }
data-testid = { testId ? testId : undefined }
id = { id }
onBlur = { _onBlur }
onChange = { _onChange }
onFocus = { _onFocus }
onKeyDown = { _onKeyDown }
placeholder = { placeholder }
ref = { inputRef }
type = { type }
value = { val } />
{val !== '' && (
<button
className = { classes.clearButton }
onClick = { _clearInput }>
<Icon
size = { 20 }
src = { IconCloseSolid } />
</button>
)}
</div>
);
}
export default ClearableInput;

View File

@ -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,

View File

@ -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 => (
<MeetingParticipantItem
askUnmuteText = { askUnmuteText }
@ -93,6 +99,7 @@ function MeetingParticipantItems({
overflowDrawer = { overflowDrawer }
participantActionEllipsisLabel = { participantActionEllipsisLabel }
participantID = { id }
searchString = { searchString }
youText = { youText } />
);

View File

@ -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
<>
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
{showInviteButton && <InviteButton />}
<ClearableInput
onChange = { setSearchString }
placeholder = { t('participantsPane.search') } />
<div>
<MeetingParticipantItems
askUnmuteText = { askUnmuteText }
@ -135,6 +141,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
participantIds = { sortedParticipantIds }
participantsCount = { participantsCount }
raiseContextId = { raiseContext.participantID }
searchString = { normalizeAccents(searchString) }
toggleMenu = { toggleMenu }
youText = { youText } />
</div>