Browse Source

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`
master
robertpin 2 years ago
parent
commit
338ff43c81
No account linked to committer's email address

+ 2
- 1
lang/main.json View File

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

+ 3
- 0
react/features/base/icons/svg/close-solid.svg View File

@@ -0,0 +1,3 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
2
+<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"/>
3
+</svg>

+ 1
- 0
react/features/base/icons/svg/index.js View File

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

+ 11
- 0
react/features/base/util/strings.native.js View File

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

+ 11
- 0
react/features/base/util/strings.web.js View File

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

+ 16
- 83
react/features/invite/components/add-people-dialog/native/AddPeopleDialog.js View File

@@ -5,8 +5,6 @@ import React from 'react';
5 5
 import {
6 6
     ActivityIndicator,
7 7
     FlatList,
8
-    Platform,
9
-    TextInput,
10 8
     TouchableOpacity,
11 9
     View
12 10
 } from 'react-native';
@@ -18,7 +16,6 @@ import {
18 16
     Icon,
19 17
     IconCancelSelection,
20 18
     IconCheck,
21
-    IconClose,
22 19
     IconPhone,
23 20
     IconSearch,
24 21
     IconShare
@@ -29,6 +26,7 @@ import {
29 26
     type Item
30 27
 } from '../../../../base/react';
31 28
 import { connect } from '../../../../base/redux';
29
+import ClearableInput from '../../../../participants-pane/components/native/ClearableInput';
32 30
 import { beginShareRoom } from '../../../../share-room';
33 31
 import { INVITE_TYPES } from '../../../constants';
34 32
 import AbstractAddPeopleDialog, {
@@ -106,11 +104,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
106 104
         selectableItems: []
107 105
     };
108 106
 
109
-    /**
110
-     * Ref of the search field.
111
-     */
112
-    inputFieldRef: ?TextInput;
113
-
114 107
     /**
115 108
      * TimeoutID to delay the search for the time the user is probably typing.
116 109
      */
@@ -136,7 +129,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
136 129
         this._onShareMeeting = this._onShareMeeting.bind(this);
137 130
         this._onTypeQuery = this._onTypeQuery.bind(this);
138 131
         this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
139
-        this._setFieldRef = this._setFieldRef.bind(this);
140 132
     }
141 133
 
142 134
     /**
@@ -220,33 +212,27 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
220 212
                 footerComponent = { this._renderShareMeetingButton }
221 213
                 hasTabNavigator = { false }
222 214
                 style = { styles.addPeopleContainer }>
223
-                <View
224
-                    style = { styles.searchFieldWrapper }>
225
-                    <View style = { styles.searchIconWrapper }>
226
-                        { this.state.searchInprogress
215
+                <ClearableInput
216
+                    autoFocus = { false }
217
+                    customStyles = {{
218
+                        wrapper: styles.searchFieldWrapper,
219
+                        input: styles.searchField,
220
+                        clearButton: styles.clearButton,
221
+                        clearIcon: styles.clearIcon
222
+                    }}
223
+                    onChange = { this._onTypeQuery }
224
+                    placeholder = { this.props.t(`inviteDialog.${placeholderKey}`) }
225
+                    placeholderColor = { palette.text04 }
226
+                    prefixComponent = { <View style = { styles.searchIconWrapper }>
227
+                        {this.state.searchInprogress
227 228
                             ? <ActivityIndicator
228 229
                                 color = { DARK_GREY }
229 230
                                 size = 'small' />
230 231
                             : <Icon
231 232
                                 src = { IconSearch }
232 233
                                 style = { styles.searchIcon } />}
233
-                    </View>
234
-                    <TextInput
235
-                        autoCorrect = { false }
236
-                        autoFocus = { false }
237
-                        onBlur = { this._onFocused(false) }
238
-                        onChangeText = { this._onTypeQuery }
239
-                        onFocus = { this._onFocused(true) }
240
-                        placeholder = {
241
-                            this.props.t(`inviteDialog.${placeholderKey}`)
242
-                        }
243
-                        placeholderTextColor = { palette.text04 }
244
-                        ref = { this._setFieldRef }
245
-                        spellCheck = { false }
246
-                        style = { styles.searchField }
247
-                        value = { this.state.fieldValue } />
248
-                    { this._renderClearButton() }
249
-                </View>
234
+                    </View> }
235
+                    value = { this.state.fieldValue } />
250 236
                 { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
251 237
                     <FlatList
252 238
                         data = { inviteItems }
@@ -337,22 +323,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
337 323
         this._onTypeQuery('');
338 324
     }
339 325
 
340
-    _onFocused: boolean => Function;
341
-
342
-    /**
343
-     * Constructs a callback to be used to update the padding of the field if necessary.
344
-     *
345
-     * @param {boolean} focused - True of the field is focused.
346
-     * @returns {Function}
347
-     */
348
-    _onFocused(focused) {
349
-        return () => {
350
-            Platform.OS === 'android' && this.setState({
351
-                bottomPadding: focused
352
-            });
353
-        };
354
-    }
355
-
356 326
     _onInvite: () => void
357 327
 
358 328
     /**
@@ -458,37 +428,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
458 428
         .finally(() => {
459 429
             this.setState({
460 430
                 searchInprogress: false
461
-            }, () => {
462
-                this.inputFieldRef && this.inputFieldRef.focus();
463 431
             });
464 432
         });
465 433
     }
466 434
 
467 435
     _query: (string) => Promise<Array<Object>>;
468 436
 
469
-    /**
470
-     * Renders a button to clear the text field.
471
-     *
472
-     * @returns {React#Element<*>}
473
-     */
474
-    _renderClearButton() {
475
-        if (!this.state.fieldValue.length) {
476
-            return null;
477
-        }
478
-
479
-        return (
480
-            <TouchableOpacity
481
-                onPress = { this._onClearField }
482
-                style = { styles.clearButton }>
483
-                <View style = { styles.clearIconContainer }>
484
-                    <Icon
485
-                        src = { IconClose }
486
-                        style = { styles.clearIcon } />
487
-                </View>
488
-            </TouchableOpacity>
489
-        );
490
-    }
491
-
492 437
     _renderInvitedItem: Object => React$Element<any> | null
493 438
 
494 439
     /**
@@ -619,18 +564,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
619 564
         );
620 565
     }
621 566
 
622
-    _setFieldRef: ?TextInput => void
623
-
624
-    /**
625
-     * Sets a reference to the input field for later use.
626
-     *
627
-     * @param {?TextInput} input - The reference to the input field.
628
-     * @returns {void}
629
-     */
630
-    _setFieldRef(input) {
631
-        this.inputFieldRef = input;
632
-    }
633
-
634 567
     /**
635 568
      * Shows an alert telling the user that some invitees were failed to be
636 569
      * invited.

+ 12
- 10
react/features/invite/components/add-people-dialog/native/styles.js View File

@@ -31,13 +31,11 @@ export default {
31 31
     },
32 32
 
33 33
     clearButton: {
34
-        alignItems: 'center',
35
-        justifyContent: 'center',
36
-        marginLeft: 5
34
+        paddingTop: 7
37 35
     },
38 36
 
39 37
     clearIcon: {
40
-        color: DARK_GREY,
38
+        color: BaseTheme.palette.ui02,
41 39
         fontSize: 18,
42 40
         textAlign: 'center'
43 41
     },
@@ -100,7 +98,9 @@ export default {
100 98
         color: DARK_GREY,
101 99
         flex: 1,
102 100
         fontSize: 17,
103
-        paddingVertical: 7
101
+        paddingVertical: 7,
102
+        paddingLeft: 0,
103
+        textAlign: 'left'
104 104
     },
105 105
 
106 106
     selectedIcon: {
@@ -117,11 +117,15 @@ export default {
117 117
     },
118 118
 
119 119
     searchFieldWrapper: {
120
+        backgroundColor: BaseTheme.palette.section01,
120 121
         alignItems: 'stretch',
121 122
         flexDirection: 'row',
122
-        height: 52,
123
-        paddingHorizontal: 12,
124
-        paddingVertical: 8
123
+        height: 36,
124
+        marginHorizontal: 15,
125
+        marginVertical: 8,
126
+        borderWidth: 0,
127
+        borderRadius: 10,
128
+        overflow: 'hidden'
125 129
     },
126 130
 
127 131
     searchIcon: {
@@ -132,8 +136,6 @@ export default {
132 136
     searchIconWrapper: {
133 137
         alignItems: 'center',
134 138
         backgroundColor: BaseTheme.palette.section01,
135
-        borderBottomLeftRadius: 10,
136
-        borderTopLeftRadius: 10,
137 139
         flexDirection: 'row',
138 140
         justifyContent: 'center',
139 141
         width: ICON_SIZE + 16

+ 195
- 0
react/features/participants-pane/components/native/ClearableInput.js View File

@@ -0,0 +1,195 @@
1
+// @flow
2
+
3
+import React, { useCallback, useEffect, useState } from 'react';
4
+import { View, TextInput, TouchableOpacity } from 'react-native';
5
+import { withTheme } from 'react-native-paper';
6
+
7
+import { Icon, IconCloseSolid } from '../../../base/icons';
8
+
9
+import styles from './styles';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * If the input should be focused on display.
15
+     */
16
+    autoFocus?: boolean,
17
+
18
+    /**
19
+     * Custom styles for the component.
20
+     */
21
+    customStyles?: Object,
22
+
23
+    /**
24
+    * Callback for the onBlur event of the field.
25
+    */
26
+    onBlur?: Function,
27
+
28
+    /**
29
+     * Callback for the onChange event of the field.
30
+     */
31
+    onChange: Function,
32
+
33
+    /**
34
+    * Callback for the onFocus event of the field.
35
+    */
36
+    onFocus?: Function,
37
+
38
+    /**
39
+     * Callback to be used when the user hits Enter in the field.
40
+     */
41
+    onSubmit?: Function,
42
+
43
+    /**
44
+     * Placeholder text for the field.
45
+     */
46
+    placeholder: string,
47
+
48
+    /**
49
+     * Placeholder text color.
50
+     */
51
+    placeholderColor?: string,
52
+
53
+    /**
54
+     * Component to be added to the beginning of the the input.
55
+     */
56
+    prefixComponent?: React$Node,
57
+
58
+    /**
59
+     * The type of the return key.
60
+     */
61
+    returnKeyType?: 'done' | 'go' | 'next' | 'search' | 'send' | 'none' | 'previous' | 'default',
62
+
63
+    /**
64
+     * Color of the caret and selection.
65
+     */
66
+    selectionColor?: string,
67
+
68
+    /**
69
+     * Theme used for styles.
70
+     */
71
+    theme: Object,
72
+
73
+    /**
74
+     * Externally provided value.
75
+     */
76
+    value?: string
77
+};
78
+
79
+/**
80
+ * Implements a pre-styled clearable input field.
81
+ *
82
+ * @param {Props} props - The props of the component.
83
+ * @returns {ReactElement}
84
+ */
85
+function ClearableInput({
86
+    autoFocus = false,
87
+    customStyles = {},
88
+    onBlur,
89
+    onChange,
90
+    onFocus,
91
+    onSubmit,
92
+    placeholder,
93
+    placeholderColor,
94
+    prefixComponent,
95
+    returnKeyType = 'search',
96
+    selectionColor,
97
+    theme,
98
+    value
99
+}: Props) {
100
+    const [ val, setVal ] = useState(value || '');
101
+    const [ focused, setFocused ] = useState(false);
102
+    const inputRef = React.createRef();
103
+
104
+    useEffect(() => {
105
+        if (value && value !== val) {
106
+            setVal(value);
107
+        }
108
+    }, [ value ]);
109
+
110
+
111
+    /**
112
+     * Callback for the onBlur event of the field.
113
+     *
114
+     * @returns {void}
115
+     */
116
+    const _onBlur = useCallback(() => {
117
+        setFocused(false);
118
+
119
+        onBlur && onBlur();
120
+    }, [ onBlur ]);
121
+
122
+    /**
123
+     * Callback for the onChange event of the field.
124
+     *
125
+     * @param {Object} evt - The static event.
126
+     * @returns {void}
127
+     */
128
+    const _onChange = useCallback(evt => {
129
+        const { nativeEvent: { text } } = evt;
130
+
131
+        setVal(text);
132
+        onChange && onChange(text);
133
+    }, [ onChange ]);
134
+
135
+    /**
136
+     * Callback for the onFocus event of the field.
137
+     *
138
+     * @returns {void}
139
+     */
140
+    const _onFocus = useCallback(() => {
141
+        setFocused(true);
142
+
143
+        onFocus && onFocus();
144
+    }, [ onFocus ]);
145
+
146
+    /**
147
+     * Clears the input.
148
+     *
149
+     * @returns {void}
150
+     */
151
+    const _clearInput = useCallback(() => {
152
+        if (inputRef.current) {
153
+            inputRef.current.focus();
154
+        }
155
+        setVal('');
156
+        onChange && onChange('');
157
+    }, [ onChange ]);
158
+
159
+    return (
160
+        <View
161
+            style = { [
162
+                styles.clearableInput,
163
+                focused ? styles.clearableInputFocus : {},
164
+                customStyles?.wrapper
165
+            ] }>
166
+            {prefixComponent}
167
+            <TextInput
168
+                autoCorrect = { false }
169
+                autoFocus = { autoFocus }
170
+                onBlur = { _onBlur }
171
+                onChange = { _onChange }
172
+                onFocus = { _onFocus }
173
+                onSubmitEditing = { onSubmit }
174
+                placeholder = { placeholder }
175
+                placeholderTextColor = { placeholderColor ?? theme.palette.text01 }
176
+                ref = { inputRef }
177
+                returnKeyType = { returnKeyType }
178
+                selectionColor = { selectionColor }
179
+                style = { [ styles.clearableInputTextInput, customStyles?.input ] }
180
+                value = { val } />
181
+            {val !== '' && (
182
+                <TouchableOpacity
183
+                    onPress = { _clearInput }
184
+                    style = { [ styles.clearButton, customStyles?.clearButton ] }>
185
+                    <Icon
186
+                        size = { 22 }
187
+                        src = { IconCloseSolid }
188
+                        style = { [ styles.clearIcon, customStyles?.clearIcon ] } />
189
+                </TouchableOpacity>
190
+            )}
191
+        </View>
192
+    );
193
+}
194
+
195
+export default withTheme(ClearableInput);

+ 3
- 5
react/features/participants-pane/components/native/MeetingParticipantItem.js View File

@@ -5,7 +5,6 @@ import React, { PureComponent } from 'react';
5 5
 import { translate } from '../../../base/i18n';
6 6
 import {
7 7
     getLocalParticipant,
8
-    getParticipantByIdOrUndefined,
9 8
     getParticipantDisplayName,
10 9
     hasRaisedHand,
11 10
     isParticipantModerator
@@ -80,9 +79,9 @@ type Props = {
80 79
     dispatch: Function,
81 80
 
82 81
     /**
83
-     * The ID of the participant.
82
+     * The participant.
84 83
      */
85
-    participantID: ?string
84
+    participant: ?Object
86 85
 };
87 86
 
88 87
 /**
@@ -171,10 +170,9 @@ class MeetingParticipantItem extends PureComponent<Props> {
171 170
  * @returns {Props}
172 171
  */
173 172
 function mapStateToProps(state, ownProps): Object {
174
-    const { participantID } = ownProps;
173
+    const { participant } = ownProps;
175 174
     const { ownerId } = state['features/shared-video'];
176 175
     const localParticipantId = getLocalParticipant(state).id;
177
-    const participant = getParticipantByIdOrUndefined(state, participantID);
178 176
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
179 177
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
180 178
     const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);

+ 81
- 15
react/features/participants-pane/components/native/MeetingParticipantList.js View File

@@ -2,30 +2,38 @@
2 2
 
3 3
 import React, { PureComponent } from 'react';
4 4
 import { FlatList, Text, View } from 'react-native';
5
-import { Button } from 'react-native-paper';
5
+import { Button, withTheme } from 'react-native-paper';
6 6
 
7 7
 import { translate } from '../../../base/i18n';
8 8
 import { Icon, IconInviteMore } from '../../../base/icons';
9
-import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
9
+import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
10 10
 import { connect } from '../../../base/redux';
11
+import { normalizeAccents } from '../../../base/util/strings';
11 12
 import { doInvitePeople } from '../../../invite/actions.native';
12 13
 import { shouldRenderInviteButton } from '../../functions';
13 14
 
15
+import ClearableInput from './ClearableInput';
14 16
 import MeetingParticipantItem from './MeetingParticipantItem';
15 17
 import styles from './styles';
16 18
 
19
+
17 20
 type Props = {
18 21
 
19 22
     /**
20
-     * The ID of the local participant.
23
+     * The local participant.
21 24
      */
22
-    _localParticipantId: string,
25
+    _localParticipant: Object,
23 26
 
24 27
     /**
25 28
      * The number of participants in the conference.
26 29
      */
27 30
     _participantsCount: number,
28 31
 
32
+    /**
33
+     * The remote participants.
34
+     */
35
+    _remoteParticipants: Map<string, Object>,
36
+
29 37
     /**
30 38
      * Whether or not to show the invite button.
31 39
      */
@@ -44,13 +52,22 @@ type Props = {
44 52
     /**
45 53
      * Translation function.
46 54
      */
47
-    t: Function
55
+    t: Function,
56
+
57
+    /**
58
+     * Theme used for styles.
59
+     */
60
+    theme: Object
48 61
 }
49 62
 
63
+type State = {
64
+    searchString: string
65
+};
66
+
50 67
 /**
51 68
  *  The meeting participant list component.
52 69
  */
53
-class MeetingParticipantList extends PureComponent<Props> {
70
+class MeetingParticipantList extends PureComponent<Props, State> {
54 71
 
55 72
     /**
56 73
      * Creates new MeetingParticipantList instance.
@@ -60,9 +77,14 @@ class MeetingParticipantList extends PureComponent<Props> {
60 77
     constructor(props: Props) {
61 78
         super(props);
62 79
 
80
+        this.state = {
81
+            searchString: ''
82
+        };
83
+
63 84
         this._keyExtractor = this._keyExtractor.bind(this);
64 85
         this._onInvite = this._onInvite.bind(this);
65 86
         this._renderParticipant = this._renderParticipant.bind(this);
87
+        this._onSearchStringChange = this._onSearchStringChange.bind(this);
66 88
     }
67 89
 
68 90
     _keyExtractor: Function;
@@ -111,11 +133,49 @@ class MeetingParticipantList extends PureComponent<Props> {
111 133
      * @returns {ReactElement}
112 134
      */
113 135
     _renderParticipant({ item/* , index, separators */ }) {
114
-        return (
115
-            <MeetingParticipantItem
116
-                key = { item }
117
-                participantID = { item } />
118
-        );
136
+        const { _localParticipant, _remoteParticipants } = this.props;
137
+        const { searchString } = this.state;
138
+        const participant = item === _localParticipant?.id ? _localParticipant : _remoteParticipants.get(item);
139
+        const displayName = participant?.name;
140
+
141
+        if (displayName) {
142
+            const names = normalizeAccents(displayName)
143
+                .toLowerCase()
144
+                .split(' ');
145
+            const lowerCaseSearch = normalizeAccents(searchString).toLowerCase();
146
+
147
+            for (const name of names) {
148
+                if (lowerCaseSearch === '' || name.startsWith(lowerCaseSearch)) {
149
+                    return (
150
+                        <MeetingParticipantItem
151
+                            key = { item }
152
+                            participant = { participant } />
153
+                    );
154
+                }
155
+            }
156
+        } else if (displayName === '' && searchString === '') {
157
+            return (
158
+                <MeetingParticipantItem
159
+                    key = { item }
160
+                    participant = { participant } />
161
+            );
162
+        }
163
+
164
+        return null;
165
+    }
166
+
167
+    _onSearchStringChange: (text: string) => void;
168
+
169
+    /**
170
+     * Handles search string changes.
171
+     *
172
+     * @param {string} text - New value of the search string.
173
+     * @returns {void}
174
+     */
175
+    _onSearchStringChange(text: string) {
176
+        this.setState({
177
+            searchString: text
178
+        });
119 179
     }
120 180
 
121 181
     /**
@@ -126,7 +186,7 @@ class MeetingParticipantList extends PureComponent<Props> {
126 186
      */
127 187
     render() {
128 188
         const {
129
-            _localParticipantId,
189
+            _localParticipant,
130 190
             _participantsCount,
131 191
             _showInviteButton,
132 192
             _sortedRemoteParticipants,
@@ -150,9 +210,13 @@ class MeetingParticipantList extends PureComponent<Props> {
150 210
                         onPress = { this._onInvite }
151 211
                         style = { styles.inviteButton } />
152 212
                 }
213
+                <ClearableInput
214
+                    onChange = { this._onSearchStringChange }
215
+                    placeholder = { t('participantsPane.search') }
216
+                    selectionColor = { this.props.theme.palette.text01 } />
153 217
                 <FlatList
154 218
                     bounces = { false }
155
-                    data = { [ _localParticipantId, ..._sortedRemoteParticipants ] }
219
+                    data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
156 220
                     horizontal = { false }
157 221
                     keyExtractor = { this._keyExtractor }
158 222
                     renderItem = { this._renderParticipant }
@@ -176,13 +240,15 @@ function _mapStateToProps(state): Object {
176 240
     const _participantsCount = getParticipantCountWithFake(state);
177 241
     const { remoteParticipants } = state['features/filmstrip'];
178 242
     const _showInviteButton = shouldRenderInviteButton(state);
243
+    const _remoteParticipants = getRemoteParticipants(state);
179 244
 
180 245
     return {
181 246
         _participantsCount,
247
+        _remoteParticipants,
182 248
         _showInviteButton,
183 249
         _sortedRemoteParticipants: remoteParticipants,
184
-        _localParticipantId: getLocalParticipant(state)?.id
250
+        _localParticipant: getLocalParticipant(state)
185 251
     };
186 252
 }
187 253
 
188
-export default translate(connect(_mapStateToProps)(MeetingParticipantList));
254
+export default translate(connect(_mapStateToProps)(withTheme(MeetingParticipantList)));

+ 48
- 0
react/features/participants-pane/components/native/styles.js View File

@@ -1,3 +1,4 @@
1
+import { MD_ITEM_HEIGHT } from '../../../base/dialog/components/native/styles';
1 2
 import BaseTheme from '../../../base/ui/components/BaseTheme.native';
2 3
 
3 4
 /**
@@ -320,5 +321,52 @@ export default {
320 321
 
321 322
     divider: {
322 323
         backgroundColor: BaseTheme.palette.dividerColor
324
+    },
325
+
326
+    clearableInput: {
327
+        display: 'flex',
328
+        height: MD_ITEM_HEIGHT,
329
+        borderWidth: 1,
330
+        borderStyle: 'solid',
331
+        borderColor: BaseTheme.palette.border02,
332
+        backgroundColor: BaseTheme.palette.uiBackground,
333
+        borderRadius: 6,
334
+        marginLeft: BaseTheme.spacing[3],
335
+        marginRight: BaseTheme.spacing[3]
336
+    },
337
+
338
+    clearableInputFocus: {
339
+        borderWidth: 3,
340
+        borderColor: BaseTheme.palette.field01Focus
341
+    },
342
+
343
+    clearButton: {
344
+        backgroundColor: 'transparent',
345
+        borderWidth: 0,
346
+        position: 'absolute',
347
+        right: 0,
348
+        top: 0,
349
+        paddingTop: 12,
350
+        paddingLeft: BaseTheme.spacing[2],
351
+        width: 40,
352
+        height: MD_ITEM_HEIGHT
353
+    },
354
+
355
+    clearIcon: {
356
+        color: BaseTheme.palette.icon02
357
+    },
358
+
359
+    clearableInputTextInput: {
360
+        backgroundColor: 'transparent',
361
+        borderWidth: 0,
362
+        height: '100%',
363
+        width: '100%',
364
+        textAlign: 'center',
365
+        color: BaseTheme.palette.text01,
366
+        paddingTop: BaseTheme.spacing[2],
367
+        paddingBottom: BaseTheme.spacing[2],
368
+        paddingLeft: BaseTheme.spacing[3],
369
+        paddingRight: BaseTheme.spacing[3],
370
+        fontSize: 16
323 371
     }
324 372
 };

+ 222
- 0
react/features/participants-pane/components/web/ClearableInput.js View File

@@ -0,0 +1,222 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/core';
4
+import React, { useCallback, useEffect, useState } from 'react';
5
+
6
+import { Icon, IconCloseSolid } from '../../../base/icons';
7
+
8
+type Props = {
9
+
10
+    /**
11
+     * String for html autocomplete attribute.
12
+    */
13
+    autoComplete?: string,
14
+
15
+    /**
16
+     * If the input should be focused on display.
17
+     */
18
+    autoFocus?: boolean,
19
+
20
+    /**
21
+     * Class name to be appended to the default class list.
22
+     */
23
+    className?: string,
24
+
25
+    /**
26
+     * Input id.
27
+     */
28
+    id?: string,
29
+
30
+    /**
31
+     * Callback for the onChange event of the field.
32
+     */
33
+    onChange: Function,
34
+
35
+    /**
36
+     * Callback to be used when the user hits Enter in the field.
37
+     */
38
+    onSubmit?: Function,
39
+
40
+    /**
41
+     * Placeholder text for the field.
42
+     */
43
+    placeholder: string,
44
+
45
+    /**
46
+     * The field type (e.g. text, password...etc).
47
+     */
48
+    type?: string,
49
+
50
+    /**
51
+     * TestId of the button. Can be used to locate element when testing UI.
52
+     */
53
+    testId?: string,
54
+
55
+    /**
56
+     * Externally provided value.
57
+     */
58
+    value?: string
59
+};
60
+
61
+const useStyles = makeStyles(theme => {
62
+    return {
63
+        clearableInput: {
64
+            display: 'flex',
65
+            alignItems: 'center',
66
+            justifyContent: 'flex-start',
67
+            height: '20px',
68
+            border: `1px solid ${theme.palette.border02}`,
69
+            backgroundColor: theme.palette.uiBackground,
70
+            position: 'relative',
71
+            borderRadius: '6px',
72
+            padding: '10px 16px',
73
+
74
+            '&.focused': {
75
+                border: `3px solid ${theme.palette.field01Focus}`
76
+            }
77
+        },
78
+        clearButton: {
79
+            backgroundColor: 'transparent',
80
+            border: 0,
81
+            position: 'absolute',
82
+            right: '10px',
83
+            top: '11px',
84
+            padding: 0,
85
+
86
+            '& svg': {
87
+                fill: theme.palette.icon02
88
+            }
89
+        },
90
+        input: {
91
+            backgroundColor: 'transparent',
92
+            border: 0,
93
+            width: '100%',
94
+            height: '100%',
95
+            borderRadius: '6px',
96
+            fontSize: '14px',
97
+            lineHeight: '20px',
98
+            textAlign: 'center',
99
+            caretColor: theme.palette.text01,
100
+            color: theme.palette.text01,
101
+
102
+            '&::placeholder': {
103
+                color: theme.palette.text03
104
+            }
105
+        }
106
+    };
107
+});
108
+
109
+/**
110
+ * Implements a pre-styled clearable input field.
111
+ *
112
+ * @param {Props} props - The props of the component.
113
+ * @returns {ReactElement}
114
+ */
115
+function ClearableInput({
116
+    autoFocus = false,
117
+    autoComplete,
118
+    className = '',
119
+    id,
120
+    onChange,
121
+    onSubmit,
122
+    placeholder,
123
+    testId,
124
+    type = 'text',
125
+    value
126
+}: Props) {
127
+    const classes = useStyles();
128
+    const [ val, setVal ] = useState(value || '');
129
+    const [ focused, setFocused ] = useState(false);
130
+    const inputRef = React.createRef();
131
+
132
+    useEffect(() => {
133
+        if (value && value !== val) {
134
+            setVal(value);
135
+        }
136
+    }, [ value ]);
137
+
138
+
139
+    /**
140
+     * Callback for the onBlur event of the field.
141
+     *
142
+     * @returns {void}
143
+     */
144
+    const _onBlur = useCallback(() => {
145
+        setFocused(false);
146
+    });
147
+
148
+    /**
149
+     * Callback for the onChange event of the field.
150
+     *
151
+     * @param {Object} evt - The static event.
152
+     * @returns {void}
153
+     */
154
+    const _onChange = useCallback(evt => {
155
+        const newValue = evt.target.value;
156
+
157
+        setVal(newValue);
158
+        onChange && onChange(newValue);
159
+    }, [ onChange ]);
160
+
161
+    /**
162
+     * Callback for the onFocus event of the field.
163
+     *
164
+     * @returns {void}
165
+     */
166
+    const _onFocus = useCallback(() => {
167
+        setFocused(true);
168
+    });
169
+
170
+    /**
171
+     * Joins the conference on 'Enter'.
172
+     *
173
+     * @param {Event} event - Key down event object.
174
+     * @returns {void}
175
+     */
176
+    const _onKeyDown = useCallback(event => {
177
+        onSubmit && event.key === 'Enter' && onSubmit();
178
+    }, [ onSubmit ]);
179
+
180
+    /**
181
+     * Clears the input.
182
+     *
183
+     * @returns {void}
184
+     */
185
+    const _clearInput = useCallback(() => {
186
+        if (inputRef.current) {
187
+            inputRef.current.focus();
188
+        }
189
+        setVal('');
190
+        onChange && onChange('');
191
+    }, [ onChange ]);
192
+
193
+    return (
194
+        <div className = { `${classes.clearableInput} ${focused ? 'focused' : ''} ${className || ''}` }>
195
+            <input
196
+                autoComplete = { autoComplete }
197
+                autoFocus = { autoFocus }
198
+                className = { classes.input }
199
+                data-testid = { testId ? testId : undefined }
200
+                id = { id }
201
+                onBlur = { _onBlur }
202
+                onChange = { _onChange }
203
+                onFocus = { _onFocus }
204
+                onKeyDown = { _onKeyDown }
205
+                placeholder = { placeholder }
206
+                ref = { inputRef }
207
+                type = { type }
208
+                value = { val } />
209
+            {val !== '' && (
210
+                <button
211
+                    className = { classes.clearButton }
212
+                    onClick = { _clearInput }>
213
+                    <Icon
214
+                        size = { 20 }
215
+                        src = { IconCloseSolid } />
216
+                </button>
217
+            )}
218
+        </div>
219
+    );
220
+}
221
+
222
+export default ClearableInput;

+ 33
- 2
react/features/participants-pane/components/web/MeetingParticipantItem.js View File

@@ -19,6 +19,7 @@ import {
19 19
     isParticipantAudioMuted,
20 20
     isParticipantVideoMuted
21 21
 } from '../../../base/tracks';
22
+import { normalizeAccents } from '../../../base/util/strings';
22 23
 import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
23 24
 import {
24 25
     getParticipantAudioMediaState,
@@ -72,6 +73,11 @@ type Props = {
72 73
      */
73 74
     _localVideoOwner: boolean,
74 75
 
76
+    /**
77
+     * Whether or not the participant name matches the search string.
78
+     */
79
+    _matchesSearch: boolean,
80
+
75 81
     /**
76 82
      * The participant.
77 83
      */
@@ -175,6 +181,7 @@ function MeetingParticipantItem({
175 181
     _displayName,
176 182
     _local,
177 183
     _localVideoOwner,
184
+    _matchesSearch,
178 185
     _participant,
179 186
     _participantID,
180 187
     _quickActionButtonType,
@@ -222,6 +229,10 @@ function MeetingParticipantItem({
222 229
         };
223 230
     }, [ _audioTrack ]);
224 231
 
232
+    if (!_matchesSearch) {
233
+        return null;
234
+    }
235
+
225 236
     const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
226 237
         ? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
227 238
 
@@ -280,12 +291,31 @@ function MeetingParticipantItem({
280 291
  * @returns {Props}
281 292
  */
282 293
 function _mapStateToProps(state, ownProps): Object {
283
-    const { participantID } = ownProps;
294
+    const { participantID, searchString } = ownProps;
284 295
     const { ownerId } = state['features/shared-video'];
285 296
     const localParticipantId = getLocalParticipant(state).id;
286 297
 
287 298
     const participant = getParticipantByIdOrUndefined(state, participantID);
288 299
 
300
+    const _displayName = getParticipantDisplayName(state, participant?.id);
301
+
302
+    let _matchesSearch = false;
303
+    const names = normalizeAccents(_displayName)
304
+        .toLowerCase()
305
+        .split(' ');
306
+    const lowerCaseSearchString = searchString.toLowerCase();
307
+
308
+    if (lowerCaseSearchString === '') {
309
+        _matchesSearch = true;
310
+    } else {
311
+        for (const name of names) {
312
+            if (name.startsWith(lowerCaseSearchString)) {
313
+                _matchesSearch = true;
314
+                break;
315
+            }
316
+        }
317
+    }
318
+
289 319
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
290 320
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
291 321
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
@@ -302,9 +332,10 @@ function _mapStateToProps(state, ownProps): Object {
302 332
         _audioMediaState,
303 333
         _audioTrack,
304 334
         _disableModeratorIndicator: disableModeratorIndicator,
305
-        _displayName: getParticipantDisplayName(state, participant?.id),
335
+        _displayName,
306 336
         _local: Boolean(participant?.local),
307 337
         _localVideoOwner: Boolean(ownerId === localParticipantId),
338
+        _matchesSearch,
308 339
         _participant: participant,
309 340
         _participantID: participant?.id,
310 341
         _quickActionButtonType,

+ 8
- 1
react/features/participants-pane/components/web/MeetingParticipantItems.js View File

@@ -56,6 +56,11 @@ type Props = {
56 56
      */
57 57
     participantActionEllipsisLabel: string,
58 58
 
59
+    /**
60
+     * Current search string.
61
+     */
62
+    searchString?: string,
63
+
59 64
     /**
60 65
      * The translated "you" text.
61 66
      */
@@ -78,8 +83,9 @@ function MeetingParticipantItems({
78 83
     overflowDrawer,
79 84
     raiseContextId,
80 85
     participantActionEllipsisLabel,
86
+    searchString,
81 87
     youText
82
-}) {
88
+}: Props) {
83 89
     const renderParticipant = id => (
84 90
         <MeetingParticipantItem
85 91
             askUnmuteText = { askUnmuteText }
@@ -93,6 +99,7 @@ function MeetingParticipantItems({
93 99
             overflowDrawer = { overflowDrawer }
94 100
             participantActionEllipsisLabel = { participantActionEllipsisLabel }
95 101
             participantID = { id }
102
+            searchString = { searchString }
96 103
             youText = { youText } />
97 104
     );
98 105
 

+ 7
- 0
react/features/participants-pane/components/web/MeetingParticipants.js View File

@@ -11,11 +11,13 @@ import {
11 11
     getParticipantCountWithFake
12 12
 } from '../../../base/participants';
13 13
 import { connect } from '../../../base/redux';
14
+import { normalizeAccents } from '../../../base/util/strings';
14 15
 import { showOverflowDrawer } from '../../../toolbox/functions';
15 16
 import { muteRemote } from '../../../video-menu/actions.any';
16 17
 import { findStyledAncestor, getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
17 18
 import { useParticipantDrawer } from '../../hooks';
18 19
 
20
+import ClearableInput from './ClearableInput';
19 21
 import { InviteButton } from './InviteButton';
20 22
 import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
21 23
 import MeetingParticipantItems from './MeetingParticipantItems';
@@ -56,6 +58,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
56 58
     const isMouseOverMenu = useRef(false);
57 59
 
58 60
     const [ raiseContext, setRaiseContext ] = useState < RaiseContext >(initialState);
61
+    const [ searchString, setSearchString ] = useState('');
59 62
     const { t } = useTranslation();
60 63
 
61 64
     const lowerMenu = useCallback(() => {
@@ -123,6 +126,9 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
123 126
         <>
124 127
             <Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
125 128
             {showInviteButton && <InviteButton />}
129
+            <ClearableInput
130
+                onChange = { setSearchString }
131
+                placeholder = { t('participantsPane.search') } />
126 132
             <div>
127 133
                 <MeetingParticipantItems
128 134
                     askUnmuteText = { askUnmuteText }
@@ -135,6 +141,7 @@ function MeetingParticipants({ participantsCount, showInviteButton, overflowDraw
135 141
                     participantIds = { sortedParticipantIds }
136 142
                     participantsCount = { participantsCount }
137 143
                     raiseContextId = { raiseContext.participantID }
144
+                    searchString = { normalizeAccents(searchString) }
138 145
                     toggleMenu = { toggleMenu }
139 146
                     youText = { youText } />
140 147
             </div>

Loading…
Cancel
Save