浏览代码

feat(native-participants-pane) added action dialogs for context menu participant details and native community slider

master
Calin Chitu 4 年前
父节点
当前提交
8d4cf7165e

+ 1
- 0
lang/main.json 查看文件

@@ -258,6 +258,7 @@
258 258
         "muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
259 259
         "muteParticipantButton": "Mute",
260 260
         "muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
261
+        "muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
261 262
         "muteParticipantTitle": "Mute this participant?",
262 263
         "muteParticipantsVideoButton": "Disable camera",
263 264
         "muteParticipantsVideoTitle": "Disable camera of this participant?",

+ 5
- 0
package-lock.json 查看文件

@@ -3102,6 +3102,11 @@
3102 3102
       "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz",
3103 3103
       "integrity": "sha512-lagdZr9UiVAccNXYfTEj+aUcPCx9ykbMe9puffeIyF3JsRuMmlu3BjHYx1klUHX7wNRmFNC8qVP0puxUt1sZ0A=="
3104 3104
     },
3105
+    "@react-native-community/slider": {
3106
+      "version": "3.0.3",
3107
+      "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-3.0.3.tgz",
3108
+      "integrity": "sha512-8IeHfDwJ9/CTUwFs6x90VlobV3BfuPgNLjTgC6dRZovfCWigaZwVNIFFJnHBakK3pW2xErAPwhdvNR4JeNoYbw=="
3109
+    },
3105 3110
     "@svgr/babel-plugin-add-jsx-attribute": {
3106 3111
       "version": "4.2.0",
3107 3112
       "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",

+ 1
- 0
package.json 查看文件

@@ -39,6 +39,7 @@
39 39
     "@react-native-async-storage/async-storage": "1.13.2",
40 40
     "@react-native-community/google-signin": "3.0.1",
41 41
     "@react-native-community/netinfo": "4.1.5",
42
+    "@react-native-community/slider": "^3.0.3",
42 43
     "@svgr/webpack": "4.3.2",
43 44
     "amplitude-js": "8.2.1",
44 45
     "base64-js": "1.3.1",

+ 1
- 1
react/features/participants-pane/components/native/ContextMenuLobbyParticipantReject.js 查看文件

@@ -39,7 +39,7 @@ export const ContextMenuLobbyParticipantReject = ({ participant: p }: Props) =>
39 39
                 <Avatar
40 40
                     className = 'participant-avatar'
41 41
                     participantId = { p.id }
42
-                    size = { 32 } />
42
+                    size = { 24 } />
43 43
                 <View style = { styles.contextMenuItemText }>
44 44
                     <Text style = { styles.contextMenuItemName }>
45 45
                         { displayName }

+ 113
- 48
react/features/participants-pane/components/native/ContextMenuMeetingParticipantDetails.js 查看文件

@@ -4,18 +4,27 @@ import React, { useCallback } from 'react';
4 4
 import { useTranslation } from 'react-i18next';
5 5
 import { TouchableOpacity, View } from 'react-native';
6 6
 import { Text } from 'react-native-paper';
7
-import { useDispatch } from 'react-redux';
7
+import { useDispatch, useSelector } from 'react-redux';
8 8
 
9 9
 import { Avatar } from '../../../base/avatar';
10
-import { hideDialog } from '../../../base/dialog';
10
+import { isToolbarButtonEnabled } from '../../../base/config';
11
+import { hideDialog, openDialog } from '../../../base/dialog';
11 12
 import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
12 13
 import {
13 14
     Icon, IconCloseCircle, IconConnectionActive, IconMessage,
14 15
     IconMicrophoneEmptySlash,
15 16
     IconMuteEveryoneElse, IconVideoOff
16 17
 } from '../../../base/icons';
17
-import { MEDIA_TYPE } from '../../../base/media';
18
-import { muteRemote } from '../../../video-menu/actions.any';
18
+import { isLocalParticipantModerator } from '../../../base/participants';
19
+import { getIsParticipantVideoMuted } from '../../../base/tracks';
20
+import { openChat } from '../../../chat/actions.native';
21
+import {
22
+    KickRemoteParticipantDialog,
23
+    MuteEveryoneDialog,
24
+    MuteRemoteParticipantDialog,
25
+    MuteRemoteParticipantsVideoDialog,
26
+    VolumeSlider
27
+} from '../../../video-menu';
19 28
 
20 29
 import styles from './styles';
21 30
 
@@ -31,7 +40,32 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
31 40
     const dispatch = useDispatch();
32 41
     const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
33 42
     const displayName = p.name;
34
-    const muteAudio = useCallback(() => dispatch(muteRemote(p.id, MEDIA_TYPE.AUDIO)), [ dispatch ]);
43
+    const isLocalModerator = useSelector(isLocalParticipantModerator);
44
+    const isChatButtonEnabled = useSelector(isToolbarButtonEnabled('chat'));
45
+    const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(p));
46
+    const kickRemoteParticipant = useCallback(() => {
47
+        dispatch(openDialog(KickRemoteParticipantDialog, {
48
+            participantID: p.id
49
+        }));
50
+    }, [ dispatch, p ]);
51
+    const muteAudio = useCallback(() => {
52
+        dispatch(openDialog(MuteRemoteParticipantDialog, {
53
+            participantID: p.id
54
+        }));
55
+    }, [ dispatch, p ]);
56
+    const muteEveryoneElse = useCallback(() => {
57
+        dispatch(openDialog(MuteEveryoneDialog, {
58
+            exclude: [ p.id ]
59
+        }));
60
+    }, [ dispatch, p ]);
61
+    const muteVideo = useCallback(() => {
62
+        dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
63
+            participantID: p.id
64
+        }));
65
+    }, [ dispatch, p ]);
66
+    const sendPrivateMessage = useCallback(() => {
67
+        dispatch(openChat(p));
68
+    }, [ dispatch, p ]);
35 69
     const { t } = useTranslation();
36 70
 
37 71
     return (
@@ -43,55 +77,85 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
43 77
                 <Avatar
44 78
                     className = 'participant-avatar'
45 79
                     participantId = { p.id }
46
-                    size = { 32 } />
80
+                    size = { 24 } />
47 81
                 <View style = { styles.contextMenuItemText }>
48 82
                     <Text style = { styles.contextMenuItemName }>
49 83
                         { displayName }
50 84
                     </Text>
51 85
                 </View>
52 86
             </View>
53
-            <TouchableOpacity
54
-                onPress = { muteAudio }
55
-                style = { styles.contextMenuItem }>
56
-                <Icon
57
-                    size = { 24 }
58
-                    src = { IconMicrophoneEmptySlash }
59
-                    style = { styles.contextMenuItemIcon } />
60
-                <Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.mute') }</Text>
61
-            </TouchableOpacity>
62
-            <TouchableOpacity
63
-                onPress = { muteAudio }
64
-                style = { styles.contextMenuItem }>
65
-                <Icon
66
-                    size = { 24 }
67
-                    src = { IconMuteEveryoneElse }
68
-                    style = { styles.contextMenuItemIcon } />
69
-                <Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.muteEveryoneElse') }</Text>
70
-            </TouchableOpacity>
71
-            <TouchableOpacity
72
-                style = { styles.contextMenuItemSection }>
73
-                <Icon
74
-                    size = { 24 }
75
-                    src = { IconVideoOff }
76
-                    style = { styles.contextMenuItemIcon } />
77
-                <Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.stopVideo') }</Text>
78
-            </TouchableOpacity>
79
-            <TouchableOpacity
80
-                style = { styles.contextMenuItem }>
81
-                <Icon
82
-                    size = { 24 }
83
-                    src = { IconCloseCircle }
84
-                    style = { styles.contextMenuItemIcon } />
85
-                <Text style = { styles.contextMenuItemText }>{ t('videothumbnail.kick') }</Text>
86
-            </TouchableOpacity>
87
-            <TouchableOpacity
88
-                style = { styles.contextMenuItem }>
89
-                <Icon
90
-                    size = { 24 }
91
-                    src = { IconMessage }
92
-                    style = { styles.contextMenuItemIcon } />
93
-                <Text style = { styles.contextMenuItemText }>{ t('toolbar.accessibilityLabel.privateMessage') }</Text>
94
-            </TouchableOpacity>
87
+            {
88
+                isLocalModerator
89
+                && <TouchableOpacity
90
+                    onPress = { muteAudio }
91
+                    style = { styles.contextMenuItem }>
92
+                    <Icon
93
+                        size = { 24 }
94
+                        src = { IconMicrophoneEmptySlash }
95
+                        style = { styles.contextMenuItemIcon } />
96
+                    <Text style = { styles.contextMenuItemText }>
97
+                        { t('participantsPane.actions.mute') }
98
+                    </Text>
99
+                </TouchableOpacity>
100
+            }
101
+            {
102
+                isLocalModerator
103
+                && <TouchableOpacity
104
+                    onPress = { muteEveryoneElse }
105
+                    style = { styles.contextMenuItem }>
106
+                    <Icon
107
+                        size = { 24 }
108
+                        src = { IconMuteEveryoneElse }
109
+                        style = { styles.contextMenuItemIcon } />
110
+                    <Text style = { styles.contextMenuItemText }>
111
+                        { t('participantsPane.actions.muteEveryoneElse') }
112
+                    </Text>
113
+                </TouchableOpacity>
114
+            }
115
+            {
116
+                isLocalModerator && (
117
+                    isParticipantVideoMuted
118
+                    || <TouchableOpacity
119
+                        onPress = { muteVideo }
120
+                        style = { styles.contextMenuItemSection }>
121
+                        <Icon
122
+                            size = { 24 }
123
+                            src = { IconVideoOff }
124
+                            style = { styles.contextMenuItemIcon } />
125
+                        <Text style = { styles.contextMenuItemText }>
126
+                            { t('participantsPane.actions.stopVideo') }
127
+                        </Text>
128
+                    </TouchableOpacity>
129
+                )
130
+            }
131
+            {
132
+                isLocalModerator
133
+                && <TouchableOpacity
134
+                    onPress = { kickRemoteParticipant }
135
+                    style = { styles.contextMenuItem }>
136
+                    <Icon
137
+                        size = { 24 }
138
+                        src = { IconCloseCircle }
139
+                        style = { styles.contextMenuItemIcon } />
140
+                    <Text style = { styles.contextMenuItemText }>
141
+                        { t('videothumbnail.kick') }
142
+                    </Text>
143
+                </TouchableOpacity>
144
+            }
145
+            {
146
+                isChatButtonEnabled
147
+                && <TouchableOpacity
148
+                    onPress = { sendPrivateMessage }
149
+                    style = { styles.contextMenuItem }>
150
+                    <Icon
151
+                        size = { 24 }
152
+                        src = { IconMessage }
153
+                        style = { styles.contextMenuItemIcon } />
154
+                    <Text style = { styles.contextMenuItemText }>
155
+                        { t('toolbar.accessibilityLabel.privateMessage') }
156
+                    </Text>
157
+                </TouchableOpacity>
158
+            }
95 159
             <TouchableOpacity
96 160
                 style = { styles.contextMenuItemSection }>
97 161
                 <Icon
@@ -100,6 +164,7 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
100 164
                     style = { styles.contextMenuItemIcon } />
101 165
                 <Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.networkStats') }</Text>
102 166
             </TouchableOpacity>
167
+            <VolumeSlider />
103 168
         </BottomSheet>
104 169
     );
105 170
 };

+ 32
- 0
react/features/video-menu/components/native/MuteRemoteParticipantsVideoDialog.js 查看文件

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { ConfirmDialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+import AbstractMuteRemoteParticipantsVideoDialog
9
+    from '../AbstractMuteRemoteParticipantsVideoDialog';
10
+
11
+/**
12
+ * Dialog to confirm a remote participant's video stop action.
13
+ */
14
+class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
15
+    /**
16
+     * Implements React's {@link Component#render()}.
17
+     *
18
+     * @inheritdoc
19
+     * @returns {ReactElement}
20
+     */
21
+    render() {
22
+        return (
23
+            <ConfirmDialog
24
+                contentKey = 'dialog.muteParticipantsVideoDialog'
25
+                onSubmit = { this._onSubmit } />
26
+        );
27
+    }
28
+
29
+    _onSubmit: () => boolean;
30
+}
31
+
32
+export default translate(connect()(MuteRemoteParticipantsVideoDialog));

+ 119
- 0
react/features/video-menu/components/native/VolumeSlider.js 查看文件

@@ -0,0 +1,119 @@
1
+// @flow
2
+
3
+import Slider from '@react-native-community/slider';
4
+import React, { Component } from 'react';
5
+import { View } from 'react-native';
6
+import { withTheme } from 'react-native-paper';
7
+
8
+import { Icon, IconVolumeEmpty } from '../../../base/icons';
9
+import { VOLUME_SLIDER_SCALE } from '../../constants';
10
+
11
+import styles from './styles';
12
+
13
+/**
14
+ * The type of the React {@code Component} props of {@link VolumeSlider}.
15
+ */
16
+type Props = {
17
+
18
+    /**
19
+     * The value of the audio slider should display at when the component first
20
+     * mounts. Changes will be stored in state. The value should be a number
21
+     * between 0 and 1.
22
+     */
23
+    initialValue: number,
24
+
25
+    /**
26
+     * The callback to invoke when the audio slider value changes.
27
+     */
28
+    onChange: Function,
29
+
30
+    /**
31
+     * Theme used for styles.
32
+     */
33
+    theme: Object
34
+};
35
+
36
+/**
37
+ * The type of the React {@code Component} state of {@link VolumeSlider}.
38
+ */
39
+type State = {
40
+
41
+    /**
42
+     * The volume of the participant's audio element. The value will
43
+     * be represented by a slider.
44
+     */
45
+    volumeLevel: number
46
+};
47
+
48
+/**
49
+ * Component that renders the volume slider.
50
+ *
51
+ * @returns {React$Element<any>}
52
+ */
53
+class VolumeSlider extends Component<Props, State> {
54
+
55
+    /**
56
+     * Initializes a new {@code VolumeSlider} instance.
57
+     *
58
+     * @param {Object} props - The read-only properties with which the new
59
+     * instance is to be initialized.
60
+     */
61
+    constructor(props: Props) {
62
+        super(props);
63
+
64
+        this.state = {
65
+            volumeLevel: (props.initialValue || 0) * VOLUME_SLIDER_SCALE
66
+        };
67
+
68
+        // Bind event handlers so they are only bound once for every instance.
69
+        this._onVolumeChange = this._onVolumeChange.bind(this);
70
+    }
71
+
72
+    /**
73
+     * Implements React's {@link Component#render()}.
74
+     *
75
+     * @inheritdoc
76
+     * @returns {ReactElement}
77
+     */
78
+    render() {
79
+        const { volumeLevel } = this.state;
80
+        const { palette } = this.props.theme;
81
+
82
+        return (
83
+            <View style = { styles.volumeSliderContainer }>
84
+                <Icon
85
+                    size = { 24 }
86
+                    src = { IconVolumeEmpty }
87
+                    style = { styles.volumeSliderIcon } />
88
+                <View style = { styles.sliderContainer }>
89
+                    <Slider
90
+                        maximumTrackTintColor = { palette.field02 }
91
+                        maximumValue = { VOLUME_SLIDER_SCALE }
92
+                        minimumTrackTintColor = { palette.action01 }
93
+                        minimumValue = { 0 }
94
+                        onValueChange = { this._onVolumeChange }
95
+                        /* eslint-disable-next-line react-native/no-inline-styles */
96
+                        value = { volumeLevel } />
97
+                </View>
98
+            </View>
99
+
100
+        );
101
+    }
102
+
103
+    _onVolumeChange: (Object) => void;
104
+
105
+    /**
106
+     * Sets the internal state of the volume level for the volume slider.
107
+     * Invokes the prop onVolumeChange to notify of volume changes.
108
+     *
109
+     * @param {number} volumeLevel - Selected volume on slider.
110
+     * @private
111
+     * @returns {void}
112
+     */
113
+    _onVolumeChange(volumeLevel) {
114
+        this.setState({ volumeLevel });
115
+    }
116
+}
117
+
118
+export default withTheme(VolumeSlider);
119
+

+ 2
- 0
react/features/video-menu/components/native/index.js 查看文件

@@ -5,4 +5,6 @@ export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantD
5 5
 export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
6 6
 export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
7 7
 export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
8
+export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
8 9
 export { default as RemoteVideoMenu } from './RemoteVideoMenu';
10
+export { default as VolumeSlider } from './VolumeSlider';

+ 18
- 0
react/features/video-menu/components/native/styles.js 查看文件

@@ -6,6 +6,7 @@ import {
6 6
     MD_ITEM_MARGIN_PADDING
7 7
 } from '../../../base/dialog';
8 8
 import { ColorPalette, createStyleSheet } from '../../../base/styles';
9
+import BaseTheme from '../../../base/ui/components/BaseTheme.native';
9 10
 
10 11
 export default createStyleSheet({
11 12
     participantNameContainer: {
@@ -48,5 +49,22 @@ export default createStyleSheet({
48 49
 
49 50
     statsWrapper: {
50 51
         marginVertical: 10
52
+    },
53
+
54
+    volumeSliderContainer: {
55
+        alignItems: 'center',
56
+        flexDirection: 'row',
57
+        marginBottom: BaseTheme.spacing[3],
58
+        marginTop: BaseTheme.spacing[3],
59
+        width: '100%'
60
+    },
61
+
62
+    volumeSliderIcon: {
63
+        marginLeft: BaseTheme.spacing[1]
64
+    },
65
+
66
+    sliderContainer: {
67
+        marginLeft: BaseTheme.spacing[3],
68
+        width: 342
51 69
     }
52 70
 });

+ 1
- 7
react/features/video-menu/components/web/VolumeSlider.js 查看文件

@@ -4,13 +4,7 @@ import React, { Component } from 'react';
4 4
 
5 5
 import { translate } from '../../../base/i18n';
6 6
 import { Icon, IconVolume } from '../../../base/icons';
7
-
8
-/**
9
- * Used to modify initialValue, which is expected to be a decimal value between
10
- * 0 and 1, and converts it to a number representable by an input slider, which
11
- * recognizes whole numbers.
12
- */
13
-const VOLUME_SLIDER_SCALE = 100;
7
+import { VOLUME_SLIDER_SCALE } from '../../constants';
14 8
 
15 9
 /**
16 10
  * The type of the React {@code Component} props of {@link VolumeSlider}.

+ 8
- 0
react/features/video-menu/constants.js 查看文件

@@ -0,0 +1,8 @@
1
+// @flow
2
+
3
+/**
4
+ * Used to modify initialValue, which is expected to be a decimal value between
5
+ * 0 and 1, and converts it to a number representable by an input slider, which
6
+ * recognizes whole numbers.
7
+ */
8
+export const VOLUME_SLIDER_SCALE = 100;

正在加载...
取消
保存