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:
parent
da5603dd9a
commit
338ff43c81
|
@ -627,7 +627,8 @@
|
||||||
"stopVideo": "Stop video",
|
"stopVideo": "Stop video",
|
||||||
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
|
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
|
||||||
"videoModeration": "Start their video"
|
"videoModeration": "Start their video"
|
||||||
}
|
},
|
||||||
|
"search": "Search participants"
|
||||||
},
|
},
|
||||||
"passwordSetRemotely": "Set by another participant",
|
"passwordSetRemotely": "Set by another participant",
|
||||||
"passwordDigitsOnly": "Up to {{number}} digits",
|
"passwordDigitsOnly": "Up to {{number}} digits",
|
||||||
|
|
|
@ -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 |
|
@ -29,6 +29,7 @@ export { default as IconCheck } from './check.svg';
|
||||||
export { default as IconCheckSolid } from './check-solid.svg';
|
export { default as IconCheckSolid } from './check-solid.svg';
|
||||||
export { default as IconClose } from './close.svg';
|
export { default as IconClose } from './close.svg';
|
||||||
export { default as IconCloseCircle } from './close-circle.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 IconCloseX } from './close-x.svg';
|
||||||
export { default as IconClosedCaption } from './closed_caption.svg';
|
export { default as IconClosedCaption } from './closed_caption.svg';
|
||||||
export { default as IconCloseSmall } from './close-small.svg';
|
export { default as IconCloseSmall } from './close-small.svg';
|
||||||
|
|
|
@ -12,3 +12,14 @@ import * as unorm from 'unorm';
|
||||||
export function normalizeNFKC(text: string) {
|
export function normalizeNFKC(text: string) {
|
||||||
return unorm.nfkc(text);
|
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, '');
|
||||||
|
}
|
||||||
|
|
|
@ -9,3 +9,14 @@
|
||||||
export function normalizeNFKC(text: string) {
|
export function normalizeNFKC(text: string) {
|
||||||
return text.normalize('NFKC');
|
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, '');
|
||||||
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import React from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
Platform,
|
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
@ -18,7 +16,6 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
IconCancelSelection,
|
IconCancelSelection,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconClose,
|
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconShare
|
IconShare
|
||||||
|
@ -29,6 +26,7 @@ import {
|
||||||
type Item
|
type Item
|
||||||
} from '../../../../base/react';
|
} from '../../../../base/react';
|
||||||
import { connect } from '../../../../base/redux';
|
import { connect } from '../../../../base/redux';
|
||||||
|
import ClearableInput from '../../../../participants-pane/components/native/ClearableInput';
|
||||||
import { beginShareRoom } from '../../../../share-room';
|
import { beginShareRoom } from '../../../../share-room';
|
||||||
import { INVITE_TYPES } from '../../../constants';
|
import { INVITE_TYPES } from '../../../constants';
|
||||||
import AbstractAddPeopleDialog, {
|
import AbstractAddPeopleDialog, {
|
||||||
|
@ -106,11 +104,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||||
selectableItems: []
|
selectableItems: []
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Ref of the search field.
|
|
||||||
*/
|
|
||||||
inputFieldRef: ?TextInput;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimeoutID to delay the search for the time the user is probably typing.
|
* 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._onShareMeeting = this._onShareMeeting.bind(this);
|
||||||
this._onTypeQuery = this._onTypeQuery.bind(this);
|
this._onTypeQuery = this._onTypeQuery.bind(this);
|
||||||
this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
|
this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
|
||||||
this._setFieldRef = this._setFieldRef.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -220,33 +212,27 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||||
footerComponent = { this._renderShareMeetingButton }
|
footerComponent = { this._renderShareMeetingButton }
|
||||||
hasTabNavigator = { false }
|
hasTabNavigator = { false }
|
||||||
style = { styles.addPeopleContainer }>
|
style = { styles.addPeopleContainer }>
|
||||||
<View
|
<ClearableInput
|
||||||
style = { styles.searchFieldWrapper }>
|
autoFocus = { false }
|
||||||
<View style = { styles.searchIconWrapper }>
|
customStyles = {{
|
||||||
{ this.state.searchInprogress
|
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
|
? <ActivityIndicator
|
||||||
color = { DARK_GREY }
|
color = { DARK_GREY }
|
||||||
size = 'small' />
|
size = 'small' />
|
||||||
: <Icon
|
: <Icon
|
||||||
src = { IconSearch }
|
src = { IconSearch }
|
||||||
style = { styles.searchIcon } />}
|
style = { styles.searchIcon } />}
|
||||||
</View>
|
</View> }
|
||||||
<TextInput
|
value = { this.state.fieldValue } />
|
||||||
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 }
|
|
||||||
value = { this.state.fieldValue } />
|
|
||||||
{ this._renderClearButton() }
|
|
||||||
</View>
|
|
||||||
{ Boolean(inviteItems.length) && <View style = { styles.invitedList }>
|
{ Boolean(inviteItems.length) && <View style = { styles.invitedList }>
|
||||||
<FlatList
|
<FlatList
|
||||||
data = { inviteItems }
|
data = { inviteItems }
|
||||||
|
@ -337,22 +323,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||||
this._onTypeQuery('');
|
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
|
_onInvite: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -458,37 +428,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchInprogress: false
|
searchInprogress: false
|
||||||
}, () => {
|
|
||||||
this.inputFieldRef && this.inputFieldRef.focus();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_query: (string) => Promise<Array<Object>>;
|
_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
|
_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
|
* Shows an alert telling the user that some invitees were failed to be
|
||||||
* invited.
|
* invited.
|
||||||
|
|
|
@ -31,13 +31,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
clearButton: {
|
clearButton: {
|
||||||
alignItems: 'center',
|
paddingTop: 7
|
||||||
justifyContent: 'center',
|
|
||||||
marginLeft: 5
|
|
||||||
},
|
},
|
||||||
|
|
||||||
clearIcon: {
|
clearIcon: {
|
||||||
color: DARK_GREY,
|
color: BaseTheme.palette.ui02,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
|
@ -100,7 +98,9 @@ export default {
|
||||||
color: DARK_GREY,
|
color: DARK_GREY,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
paddingVertical: 7
|
paddingVertical: 7,
|
||||||
|
paddingLeft: 0,
|
||||||
|
textAlign: 'left'
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedIcon: {
|
selectedIcon: {
|
||||||
|
@ -117,11 +117,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
searchFieldWrapper: {
|
searchFieldWrapper: {
|
||||||
|
backgroundColor: BaseTheme.palette.section01,
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
height: 52,
|
height: 36,
|
||||||
paddingHorizontal: 12,
|
marginHorizontal: 15,
|
||||||
paddingVertical: 8
|
marginVertical: 8,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: 'hidden'
|
||||||
},
|
},
|
||||||
|
|
||||||
searchIcon: {
|
searchIcon: {
|
||||||
|
@ -132,8 +136,6 @@ export default {
|
||||||
searchIconWrapper: {
|
searchIconWrapper: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: BaseTheme.palette.section01,
|
backgroundColor: BaseTheme.palette.section01,
|
||||||
borderBottomLeftRadius: 10,
|
|
||||||
borderTopLeftRadius: 10,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: ICON_SIZE + 16
|
width: ICON_SIZE + 16
|
||||||
|
|
|
@ -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);
|
|
@ -5,7 +5,6 @@ import React, { PureComponent } from 'react';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipantByIdOrUndefined,
|
|
||||||
getParticipantDisplayName,
|
getParticipantDisplayName,
|
||||||
hasRaisedHand,
|
hasRaisedHand,
|
||||||
isParticipantModerator
|
isParticipantModerator
|
||||||
|
@ -80,9 +79,9 @@ type Props = {
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the participant.
|
* The participant.
|
||||||
*/
|
*/
|
||||||
participantID: ?string
|
participant: ?Object
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,10 +170,9 @@ class MeetingParticipantItem extends PureComponent<Props> {
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
function mapStateToProps(state, ownProps): Object {
|
function mapStateToProps(state, ownProps): Object {
|
||||||
const { participantID } = ownProps;
|
const { participant } = ownProps;
|
||||||
const { ownerId } = state['features/shared-video'];
|
const { ownerId } = state['features/shared-video'];
|
||||||
const localParticipantId = getLocalParticipant(state).id;
|
const localParticipantId = getLocalParticipant(state).id;
|
||||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
|
||||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||||
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||||
|
|
|
@ -2,30 +2,38 @@
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { FlatList, Text, View } from 'react-native';
|
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 { translate } from '../../../base/i18n';
|
||||||
import { Icon, IconInviteMore } from '../../../base/icons';
|
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 { connect } from '../../../base/redux';
|
||||||
|
import { normalizeAccents } from '../../../base/util/strings';
|
||||||
import { doInvitePeople } from '../../../invite/actions.native';
|
import { doInvitePeople } from '../../../invite/actions.native';
|
||||||
import { shouldRenderInviteButton } from '../../functions';
|
import { shouldRenderInviteButton } from '../../functions';
|
||||||
|
|
||||||
|
import ClearableInput from './ClearableInput';
|
||||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the local participant.
|
* The local participant.
|
||||||
*/
|
*/
|
||||||
_localParticipantId: string,
|
_localParticipant: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of participants in the conference.
|
* The number of participants in the conference.
|
||||||
*/
|
*/
|
||||||
_participantsCount: number,
|
_participantsCount: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remote participants.
|
||||||
|
*/
|
||||||
|
_remoteParticipants: Map<string, Object>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to show the invite button.
|
* Whether or not to show the invite button.
|
||||||
*/
|
*/
|
||||||
|
@ -44,13 +52,22 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Translation function.
|
* Translation function.
|
||||||
*/
|
*/
|
||||||
t: Function
|
t: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme used for styles.
|
||||||
|
*/
|
||||||
|
theme: Object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
searchString: string
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The meeting participant list component.
|
* The meeting participant list component.
|
||||||
*/
|
*/
|
||||||
class MeetingParticipantList extends PureComponent<Props> {
|
class MeetingParticipantList extends PureComponent<Props, State> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new MeetingParticipantList instance.
|
* Creates new MeetingParticipantList instance.
|
||||||
|
@ -60,9 +77,14 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchString: ''
|
||||||
|
};
|
||||||
|
|
||||||
this._keyExtractor = this._keyExtractor.bind(this);
|
this._keyExtractor = this._keyExtractor.bind(this);
|
||||||
this._onInvite = this._onInvite.bind(this);
|
this._onInvite = this._onInvite.bind(this);
|
||||||
this._renderParticipant = this._renderParticipant.bind(this);
|
this._renderParticipant = this._renderParticipant.bind(this);
|
||||||
|
this._onSearchStringChange = this._onSearchStringChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_keyExtractor: Function;
|
_keyExtractor: Function;
|
||||||
|
@ -111,11 +133,49 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
_renderParticipant({ item/* , index, separators */ }) {
|
_renderParticipant({ item/* , index, separators */ }) {
|
||||||
return (
|
const { _localParticipant, _remoteParticipants } = this.props;
|
||||||
<MeetingParticipantItem
|
const { searchString } = this.state;
|
||||||
key = { item }
|
const participant = item === _localParticipant?.id ? _localParticipant : _remoteParticipants.get(item);
|
||||||
participantID = { 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 }
|
||||||
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,7 +186,7 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
_localParticipantId,
|
_localParticipant,
|
||||||
_participantsCount,
|
_participantsCount,
|
||||||
_showInviteButton,
|
_showInviteButton,
|
||||||
_sortedRemoteParticipants,
|
_sortedRemoteParticipants,
|
||||||
|
@ -150,9 +210,13 @@ class MeetingParticipantList extends PureComponent<Props> {
|
||||||
onPress = { this._onInvite }
|
onPress = { this._onInvite }
|
||||||
style = { styles.inviteButton } />
|
style = { styles.inviteButton } />
|
||||||
}
|
}
|
||||||
|
<ClearableInput
|
||||||
|
onChange = { this._onSearchStringChange }
|
||||||
|
placeholder = { t('participantsPane.search') }
|
||||||
|
selectionColor = { this.props.theme.palette.text01 } />
|
||||||
<FlatList
|
<FlatList
|
||||||
bounces = { false }
|
bounces = { false }
|
||||||
data = { [ _localParticipantId, ..._sortedRemoteParticipants ] }
|
data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
|
||||||
horizontal = { false }
|
horizontal = { false }
|
||||||
keyExtractor = { this._keyExtractor }
|
keyExtractor = { this._keyExtractor }
|
||||||
renderItem = { this._renderParticipant }
|
renderItem = { this._renderParticipant }
|
||||||
|
@ -176,13 +240,15 @@ function _mapStateToProps(state): Object {
|
||||||
const _participantsCount = getParticipantCountWithFake(state);
|
const _participantsCount = getParticipantCountWithFake(state);
|
||||||
const { remoteParticipants } = state['features/filmstrip'];
|
const { remoteParticipants } = state['features/filmstrip'];
|
||||||
const _showInviteButton = shouldRenderInviteButton(state);
|
const _showInviteButton = shouldRenderInviteButton(state);
|
||||||
|
const _remoteParticipants = getRemoteParticipants(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_participantsCount,
|
_participantsCount,
|
||||||
|
_remoteParticipants,
|
||||||
_showInviteButton,
|
_showInviteButton,
|
||||||
_sortedRemoteParticipants: remoteParticipants,
|
_sortedRemoteParticipants: remoteParticipants,
|
||||||
_localParticipantId: getLocalParticipant(state)?.id
|
_localParticipant: getLocalParticipant(state)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(connect(_mapStateToProps)(MeetingParticipantList));
|
export default translate(connect(_mapStateToProps)(withTheme(MeetingParticipantList)));
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { MD_ITEM_HEIGHT } from '../../../base/dialog/components/native/styles';
|
||||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,5 +321,52 @@ export default {
|
||||||
|
|
||||||
divider: {
|
divider: {
|
||||||
backgroundColor: BaseTheme.palette.dividerColor
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
@ -19,6 +19,7 @@ import {
|
||||||
isParticipantAudioMuted,
|
isParticipantAudioMuted,
|
||||||
isParticipantVideoMuted
|
isParticipantVideoMuted
|
||||||
} from '../../../base/tracks';
|
} from '../../../base/tracks';
|
||||||
|
import { normalizeAccents } from '../../../base/util/strings';
|
||||||
import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
|
import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
|
||||||
import {
|
import {
|
||||||
getParticipantAudioMediaState,
|
getParticipantAudioMediaState,
|
||||||
|
@ -72,6 +73,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_localVideoOwner: boolean,
|
_localVideoOwner: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the participant name matches the search string.
|
||||||
|
*/
|
||||||
|
_matchesSearch: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The participant.
|
* The participant.
|
||||||
*/
|
*/
|
||||||
|
@ -175,6 +181,7 @@ function MeetingParticipantItem({
|
||||||
_displayName,
|
_displayName,
|
||||||
_local,
|
_local,
|
||||||
_localVideoOwner,
|
_localVideoOwner,
|
||||||
|
_matchesSearch,
|
||||||
_participant,
|
_participant,
|
||||||
_participantID,
|
_participantID,
|
||||||
_quickActionButtonType,
|
_quickActionButtonType,
|
||||||
|
@ -222,6 +229,10 @@ function MeetingParticipantItem({
|
||||||
};
|
};
|
||||||
}, [ _audioTrack ]);
|
}, [ _audioTrack ]);
|
||||||
|
|
||||||
|
if (!_matchesSearch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
|
||||||
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
|
||||||
|
|
||||||
|
@ -280,12 +291,31 @@ function MeetingParticipantItem({
|
||||||
* @returns {Props}
|
* @returns {Props}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state, ownProps): Object {
|
function _mapStateToProps(state, ownProps): Object {
|
||||||
const { participantID } = ownProps;
|
const { participantID, searchString } = ownProps;
|
||||||
const { ownerId } = state['features/shared-video'];
|
const { ownerId } = state['features/shared-video'];
|
||||||
const localParticipantId = getLocalParticipant(state).id;
|
const localParticipantId = getLocalParticipant(state).id;
|
||||||
|
|
||||||
const participant = getParticipantByIdOrUndefined(state, participantID);
|
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 _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||||
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
|
||||||
|
@ -302,9 +332,10 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
_audioMediaState,
|
_audioMediaState,
|
||||||
_audioTrack,
|
_audioTrack,
|
||||||
_disableModeratorIndicator: disableModeratorIndicator,
|
_disableModeratorIndicator: disableModeratorIndicator,
|
||||||
_displayName: getParticipantDisplayName(state, participant?.id),
|
_displayName,
|
||||||
_local: Boolean(participant?.local),
|
_local: Boolean(participant?.local),
|
||||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||||
|
_matchesSearch,
|
||||||
_participant: participant,
|
_participant: participant,
|
||||||
_participantID: participant?.id,
|
_participantID: participant?.id,
|
||||||
_quickActionButtonType,
|
_quickActionButtonType,
|
||||||
|
|
|
@ -56,6 +56,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
participantActionEllipsisLabel: string,
|
participantActionEllipsisLabel: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current search string.
|
||||||
|
*/
|
||||||
|
searchString?: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translated "you" text.
|
* The translated "you" text.
|
||||||
*/
|
*/
|
||||||
|
@ -78,8 +83,9 @@ function MeetingParticipantItems({
|
||||||
overflowDrawer,
|
overflowDrawer,
|
||||||
raiseContextId,
|
raiseContextId,
|
||||||
participantActionEllipsisLabel,
|
participantActionEllipsisLabel,
|
||||||
|
searchString,
|
||||||
youText
|
youText
|
||||||
}) {
|
}: Props) {
|
||||||
const renderParticipant = id => (
|
const renderParticipant = id => (
|
||||||
<MeetingParticipantItem
|
<MeetingParticipantItem
|
||||||
askUnmuteText = { askUnmuteText }
|
askUnmuteText = { askUnmuteText }
|
||||||
|
@ -93,6 +99,7 @@ function MeetingParticipantItems({
|
||||||
overflowDrawer = { overflowDrawer }
|
overflowDrawer = { overflowDrawer }
|
||||||
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
participantActionEllipsisLabel = { participantActionEllipsisLabel }
|
||||||
participantID = { id }
|
participantID = { id }
|
||||||
|
searchString = { searchString }
|
||||||
youText = { youText } />
|
youText = { youText } />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,13 @@ import {
|
||||||
getParticipantCountWithFake
|
getParticipantCountWithFake
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { normalizeAccents } from '../../../base/util/strings';
|
||||||
import { showOverflowDrawer } from '../../../toolbox/functions';
|
import { showOverflowDrawer } from '../../../toolbox/functions';
|
||||||
import { muteRemote } from '../../../video-menu/actions.any';
|
import { muteRemote } from '../../../video-menu/actions.any';
|
||||||
import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
|
import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
|
||||||
import { useParticipantDrawer } from '../../hooks';
|
import { useParticipantDrawer } from '../../hooks';
|
||||||
|
|
||||||
|
import ClearableInput from './ClearableInput';
|
||||||
import { InviteButton } from './InviteButton';
|
import { InviteButton } from './InviteButton';
|
||||||
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
|
||||||
import MeetingParticipantItems from './MeetingParticipantItems';
|
import MeetingParticipantItems from './MeetingParticipantItems';
|
||||||
|
@ -56,6 +58,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
|
||||||
const isMouseOverMenu = useRef(false);
|
const isMouseOverMenu = useRef(false);
|
||||||
|
|
||||||
const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
|
const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
|
||||||
|
const [ searchString, setSearchString ] = useState('');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const lowerMenu = useCallback(() => {
|
const lowerMenu = useCallback(() => {
|
||||||
|
@ -123,6 +126,9 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
|
||||||
<>
|
<>
|
||||||
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
||||||
{showInviteButton && <InviteButton />}
|
{showInviteButton && <InviteButton />}
|
||||||
|
<ClearableInput
|
||||||
|
onChange = { setSearchString }
|
||||||
|
placeholder = { t('participantsPane.search') } />
|
||||||
<div>
|
<div>
|
||||||
<MeetingParticipantItems
|
<MeetingParticipantItems
|
||||||
askUnmuteText = { askUnmuteText }
|
askUnmuteText = { askUnmuteText }
|
||||||
|
@ -135,6 +141,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
|
||||||
participantIds = { sortedParticipantIds }
|
participantIds = { sortedParticipantIds }
|
||||||
participantsCount = { participantsCount }
|
participantsCount = { participantsCount }
|
||||||
raiseContextId = { raiseContext.participantID }
|
raiseContextId = { raiseContext.participantID }
|
||||||
|
searchString = { normalizeAccents(searchString) }
|
||||||
toggleMenu = { toggleMenu }
|
toggleMenu = { toggleMenu }
|
||||||
youText = { youText } />
|
youText = { youText } />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue