Ver código fonte

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 3 anos atrás
pai
commit
338ff43c81
Nenhuma conta vinculada ao e-mail do autor do commit

+ 2
- 1
lang/main.json Ver arquivo

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

+ 3
- 0
react/features/base/icons/svg/close-solid.svg Ver arquivo

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 Ver arquivo

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

+ 11
- 0
react/features/base/util/strings.native.js Ver arquivo

12
 export function normalizeNFKC(text: string) {
12
 export function normalizeNFKC(text: string) {
13
     return unorm.nfkc(text);
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 Ver arquivo

9
 export function normalizeNFKC(text: string) {
9
 export function normalizeNFKC(text: string) {
10
     return text.normalize('NFKC');
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 Ver arquivo

5
 import {
5
 import {
6
     ActivityIndicator,
6
     ActivityIndicator,
7
     FlatList,
7
     FlatList,
8
-    Platform,
9
-    TextInput,
10
     TouchableOpacity,
8
     TouchableOpacity,
11
     View
9
     View
12
 } from 'react-native';
10
 } from 'react-native';
18
     Icon,
16
     Icon,
19
     IconCancelSelection,
17
     IconCancelSelection,
20
     IconCheck,
18
     IconCheck,
21
-    IconClose,
22
     IconPhone,
19
     IconPhone,
23
     IconSearch,
20
     IconSearch,
24
     IconShare
21
     IconShare
29
     type Item
26
     type Item
30
 } from '../../../../base/react';
27
 } from '../../../../base/react';
31
 import { connect } from '../../../../base/redux';
28
 import { connect } from '../../../../base/redux';
29
+import ClearableInput from '../../../../participants-pane/components/native/ClearableInput';
32
 import { beginShareRoom } from '../../../../share-room';
30
 import { beginShareRoom } from '../../../../share-room';
33
 import { INVITE_TYPES } from '../../../constants';
31
 import { INVITE_TYPES } from '../../../constants';
34
 import AbstractAddPeopleDialog, {
32
 import AbstractAddPeopleDialog, {
106
         selectableItems: []
104
         selectableItems: []
107
     };
105
     };
108
 
106
 
109
-    /**
110
-     * Ref of the search field.
111
-     */
112
-    inputFieldRef: ?TextInput;
113
-
114
     /**
107
     /**
115
      * TimeoutID to delay the search for the time the user is probably typing.
108
      * TimeoutID to delay the search for the time the user is probably typing.
116
      */
109
      */
136
         this._onShareMeeting = this._onShareMeeting.bind(this);
129
         this._onShareMeeting = this._onShareMeeting.bind(this);
137
         this._onTypeQuery = this._onTypeQuery.bind(this);
130
         this._onTypeQuery = this._onTypeQuery.bind(this);
138
         this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
131
         this._renderShareMeetingButton = this._renderShareMeetingButton.bind(this);
139
-        this._setFieldRef = this._setFieldRef.bind(this);
140
     }
132
     }
141
 
133
 
142
     /**
134
     /**
220
                 footerComponent = { this._renderShareMeetingButton }
212
                 footerComponent = { this._renderShareMeetingButton }
221
                 hasTabNavigator = { false }
213
                 hasTabNavigator = { false }
222
                 style = { styles.addPeopleContainer }>
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
                             ? <ActivityIndicator
228
                             ? <ActivityIndicator
228
                                 color = { DARK_GREY }
229
                                 color = { DARK_GREY }
229
                                 size = 'small' />
230
                                 size = 'small' />
230
                             : <Icon
231
                             : <Icon
231
                                 src = { IconSearch }
232
                                 src = { IconSearch }
232
                                 style = { styles.searchIcon } />}
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
                 { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
236
                 { Boolean(inviteItems.length) && <View style = { styles.invitedList }>
251
                     <FlatList
237
                     <FlatList
252
                         data = { inviteItems }
238
                         data = { inviteItems }
337
         this._onTypeQuery('');
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
     _onInvite: () => void
326
     _onInvite: () => void
357
 
327
 
358
     /**
328
     /**
458
         .finally(() => {
428
         .finally(() => {
459
             this.setState({
429
             this.setState({
460
                 searchInprogress: false
430
                 searchInprogress: false
461
-            }, () => {
462
-                this.inputFieldRef && this.inputFieldRef.focus();
463
             });
431
             });
464
         });
432
         });
465
     }
433
     }
466
 
434
 
467
     _query: (string) => Promise<Array<Object>>;
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
     _renderInvitedItem: Object => React$Element<any> | null
437
     _renderInvitedItem: Object => React$Element<any> | null
493
 
438
 
494
     /**
439
     /**
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
      * Shows an alert telling the user that some invitees were failed to be
568
      * Shows an alert telling the user that some invitees were failed to be
636
      * invited.
569
      * invited.

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

31
     },
31
     },
32
 
32
 
33
     clearButton: {
33
     clearButton: {
34
-        alignItems: 'center',
35
-        justifyContent: 'center',
36
-        marginLeft: 5
34
+        paddingTop: 7
37
     },
35
     },
38
 
36
 
39
     clearIcon: {
37
     clearIcon: {
40
-        color: DARK_GREY,
38
+        color: BaseTheme.palette.ui02,
41
         fontSize: 18,
39
         fontSize: 18,
42
         textAlign: 'center'
40
         textAlign: 'center'
43
     },
41
     },
100
         color: DARK_GREY,
98
         color: DARK_GREY,
101
         flex: 1,
99
         flex: 1,
102
         fontSize: 17,
100
         fontSize: 17,
103
-        paddingVertical: 7
101
+        paddingVertical: 7,
102
+        paddingLeft: 0,
103
+        textAlign: 'left'
104
     },
104
     },
105
 
105
 
106
     selectedIcon: {
106
     selectedIcon: {
117
     },
117
     },
118
 
118
 
119
     searchFieldWrapper: {
119
     searchFieldWrapper: {
120
+        backgroundColor: BaseTheme.palette.section01,
120
         alignItems: 'stretch',
121
         alignItems: 'stretch',
121
         flexDirection: 'row',
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
     searchIcon: {
131
     searchIcon: {
132
     searchIconWrapper: {
136
     searchIconWrapper: {
133
         alignItems: 'center',
137
         alignItems: 'center',
134
         backgroundColor: BaseTheme.palette.section01,
138
         backgroundColor: BaseTheme.palette.section01,
135
-        borderBottomLeftRadius: 10,
136
-        borderTopLeftRadius: 10,
137
         flexDirection: 'row',
139
         flexDirection: 'row',
138
         justifyContent: 'center',
140
         justifyContent: 'center',
139
         width: ICON_SIZE + 16
141
         width: ICON_SIZE + 16

+ 195
- 0
react/features/participants-pane/components/native/ClearableInput.js Ver arquivo

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 Ver arquivo

5
 import { translate } from '../../../base/i18n';
5
 import { translate } from '../../../base/i18n';
6
 import {
6
 import {
7
     getLocalParticipant,
7
     getLocalParticipant,
8
-    getParticipantByIdOrUndefined,
9
     getParticipantDisplayName,
8
     getParticipantDisplayName,
10
     hasRaisedHand,
9
     hasRaisedHand,
11
     isParticipantModerator
10
     isParticipantModerator
80
     dispatch: Function,
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
  * @returns {Props}
170
  * @returns {Props}
172
  */
171
  */
173
 function mapStateToProps(state, ownProps): Object {
172
 function mapStateToProps(state, ownProps): Object {
174
-    const { participantID } = ownProps;
173
+    const { participant } = ownProps;
175
     const { ownerId } = state['features/shared-video'];
174
     const { ownerId } = state['features/shared-video'];
176
     const localParticipantId = getLocalParticipant(state).id;
175
     const localParticipantId = getLocalParticipant(state).id;
177
-    const participant = getParticipantByIdOrUndefined(state, participantID);
178
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
176
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
179
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
177
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
180
     const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
178
     const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);

+ 81
- 15
react/features/participants-pane/components/native/MeetingParticipantList.js Ver arquivo

2
 
2
 
3
 import React, { PureComponent } from 'react';
3
 import React, { PureComponent } from 'react';
4
 import { FlatList, Text, View } from 'react-native';
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
 import { translate } from '../../../base/i18n';
7
 import { translate } from '../../../base/i18n';
8
 import { Icon, IconInviteMore } from '../../../base/icons';
8
 import { Icon, IconInviteMore } from '../../../base/icons';
9
-import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
9
+import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
10
 import { connect } from '../../../base/redux';
10
 import { connect } from '../../../base/redux';
11
+import { normalizeAccents } from '../../../base/util/strings';
11
 import { doInvitePeople } from '../../../invite/actions.native';
12
 import { doInvitePeople } from '../../../invite/actions.native';
12
 import { shouldRenderInviteButton } from '../../functions';
13
 import { shouldRenderInviteButton } from '../../functions';
13
 
14
 
15
+import ClearableInput from './ClearableInput';
14
 import MeetingParticipantItem from './MeetingParticipantItem';
16
 import MeetingParticipantItem from './MeetingParticipantItem';
15
 import styles from './styles';
17
 import styles from './styles';
16
 
18
 
19
+
17
 type Props = {
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
      * The number of participants in the conference.
28
      * The number of participants in the conference.
26
      */
29
      */
27
     _participantsCount: number,
30
     _participantsCount: number,
28
 
31
 
32
+    /**
33
+     * The remote participants.
34
+     */
35
+    _remoteParticipants: Map<string, Object>,
36
+
29
     /**
37
     /**
30
      * Whether or not to show the invite button.
38
      * Whether or not to show the invite button.
31
      */
39
      */
44
     /**
52
     /**
45
      * Translation function.
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
  *  The meeting participant list component.
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
      * Creates new MeetingParticipantList instance.
73
      * Creates new MeetingParticipantList instance.
60
     constructor(props: Props) {
77
     constructor(props: Props) {
61
         super(props);
78
         super(props);
62
 
79
 
80
+        this.state = {
81
+            searchString: ''
82
+        };
83
+
63
         this._keyExtractor = this._keyExtractor.bind(this);
84
         this._keyExtractor = this._keyExtractor.bind(this);
64
         this._onInvite = this._onInvite.bind(this);
85
         this._onInvite = this._onInvite.bind(this);
65
         this._renderParticipant = this._renderParticipant.bind(this);
86
         this._renderParticipant = this._renderParticipant.bind(this);
87
+        this._onSearchStringChange = this._onSearchStringChange.bind(this);
66
     }
88
     }
67
 
89
 
68
     _keyExtractor: Function;
90
     _keyExtractor: Function;
111
      * @returns {ReactElement}
133
      * @returns {ReactElement}
112
      */
134
      */
113
     _renderParticipant({ item/* , index, separators */ }) {
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
      */
186
      */
127
     render() {
187
     render() {
128
         const {
188
         const {
129
-            _localParticipantId,
189
+            _localParticipant,
130
             _participantsCount,
190
             _participantsCount,
131
             _showInviteButton,
191
             _showInviteButton,
132
             _sortedRemoteParticipants,
192
             _sortedRemoteParticipants,
150
                         onPress = { this._onInvite }
210
                         onPress = { this._onInvite }
151
                         style = { styles.inviteButton } />
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
                 <FlatList
217
                 <FlatList
154
                     bounces = { false }
218
                     bounces = { false }
155
-                    data = { [ _localParticipantId, ..._sortedRemoteParticipants ] }
219
+                    data = { [ _localParticipant?.id, ..._sortedRemoteParticipants ] }
156
                     horizontal = { false }
220
                     horizontal = { false }
157
                     keyExtractor = { this._keyExtractor }
221
                     keyExtractor = { this._keyExtractor }
158
                     renderItem = { this._renderParticipant }
222
                     renderItem = { this._renderParticipant }
176
     const _participantsCount = getParticipantCountWithFake(state);
240
     const _participantsCount = getParticipantCountWithFake(state);
177
     const { remoteParticipants } = state['features/filmstrip'];
241
     const { remoteParticipants } = state['features/filmstrip'];
178
     const _showInviteButton = shouldRenderInviteButton(state);
242
     const _showInviteButton = shouldRenderInviteButton(state);
243
+    const _remoteParticipants = getRemoteParticipants(state);
179
 
244
 
180
     return {
245
     return {
181
         _participantsCount,
246
         _participantsCount,
247
+        _remoteParticipants,
182
         _showInviteButton,
248
         _showInviteButton,
183
         _sortedRemoteParticipants: remoteParticipants,
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 Ver arquivo

1
+import { MD_ITEM_HEIGHT } from '../../../base/dialog/components/native/styles';
1
 import BaseTheme from '../../../base/ui/components/BaseTheme.native';
2
 import BaseTheme from '../../../base/ui/components/BaseTheme.native';
2
 
3
 
3
 /**
4
 /**
320
 
321
 
321
     divider: {
322
     divider: {
322
         backgroundColor: BaseTheme.palette.dividerColor
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 Ver arquivo

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 Ver arquivo

19
     isParticipantAudioMuted,
19
     isParticipantAudioMuted,
20
     isParticipantVideoMuted
20
     isParticipantVideoMuted
21
 } from '../../../base/tracks';
21
 } from '../../../base/tracks';
22
+import { normalizeAccents } from '../../../base/util/strings';
22
 import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
23
 import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
23
 import {
24
 import {
24
     getParticipantAudioMediaState,
25
     getParticipantAudioMediaState,
72
      */
73
      */
73
     _localVideoOwner: boolean,
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
      * The participant.
82
      * The participant.
77
      */
83
      */
175
     _displayName,
181
     _displayName,
176
     _local,
182
     _local,
177
     _localVideoOwner,
183
     _localVideoOwner,
184
+    _matchesSearch,
178
     _participant,
185
     _participant,
179
     _participantID,
186
     _participantID,
180
     _quickActionButtonType,
187
     _quickActionButtonType,
222
         };
229
         };
223
     }, [ _audioTrack ]);
230
     }, [ _audioTrack ]);
224
 
231
 
232
+    if (!_matchesSearch) {
233
+        return null;
234
+    }
235
+
225
     const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
236
     const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
226
         ? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
237
         ? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
227
 
238
 
280
  * @returns {Props}
291
  * @returns {Props}
281
  */
292
  */
282
 function _mapStateToProps(state, ownProps): Object {
293
 function _mapStateToProps(state, ownProps): Object {
283
-    const { participantID } = ownProps;
294
+    const { participantID, searchString } = ownProps;
284
     const { ownerId } = state['features/shared-video'];
295
     const { ownerId } = state['features/shared-video'];
285
     const localParticipantId = getLocalParticipant(state).id;
296
     const localParticipantId = getLocalParticipant(state).id;
286
 
297
 
287
     const participant = getParticipantByIdOrUndefined(state, participantID);
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
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
319
     const _isAudioMuted = isParticipantAudioMuted(participant, state);
290
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
320
     const _isVideoMuted = isParticipantVideoMuted(participant, state);
291
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
321
     const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
302
         _audioMediaState,
332
         _audioMediaState,
303
         _audioTrack,
333
         _audioTrack,
304
         _disableModeratorIndicator: disableModeratorIndicator,
334
         _disableModeratorIndicator: disableModeratorIndicator,
305
-        _displayName: getParticipantDisplayName(state, participant?.id),
335
+        _displayName,
306
         _local: Boolean(participant?.local),
336
         _local: Boolean(participant?.local),
307
         _localVideoOwner: Boolean(ownerId === localParticipantId),
337
         _localVideoOwner: Boolean(ownerId === localParticipantId),
338
+        _matchesSearch,
308
         _participant: participant,
339
         _participant: participant,
309
         _participantID: participant?.id,
340
         _participantID: participant?.id,
310
         _quickActionButtonType,
341
         _quickActionButtonType,

+ 8
- 1
react/features/participants-pane/components/web/MeetingParticipantItems.js Ver arquivo

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

+ 7
- 0
react/features/participants-pane/components/web/MeetingParticipants.js Ver arquivo

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

Carregando…
Cancelar
Salvar