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

This commit is contained in:
Calin Chitu 2021-06-07 18:19:01 +03:00 committed by Hristo Terezov
parent 0b3991d9e1
commit 8d4cf7165e
11 changed files with 301 additions and 56 deletions

View File

@ -258,6 +258,7 @@
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"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.",
"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.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantsVideoButton": "Disable camera",
"muteParticipantsVideoTitle": "Disable camera of this participant?",

5
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -4,18 +4,27 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity, View } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { Avatar } from '../../../base/avatar';
import { hideDialog } from '../../../base/dialog';
import { isToolbarButtonEnabled } from '../../../base/config';
import { hideDialog, openDialog } from '../../../base/dialog';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import {
Icon, IconCloseCircle, IconConnectionActive, IconMessage,
IconMicrophoneEmptySlash,
IconMuteEveryoneElse, IconVideoOff
} from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { muteRemote } from '../../../video-menu/actions.any';
import { isLocalParticipantModerator } from '../../../base/participants';
import { getIsParticipantVideoMuted } from '../../../base/tracks';
import { openChat } from '../../../chat/actions.native';
import {
KickRemoteParticipantDialog,
MuteEveryoneDialog,
MuteRemoteParticipantDialog,
MuteRemoteParticipantsVideoDialog,
VolumeSlider
} from '../../../video-menu';
import styles from './styles';
@ -31,7 +40,32 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
const dispatch = useDispatch();
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
const displayName = p.name;
const muteAudio = useCallback(() => dispatch(muteRemote(p.id, MEDIA_TYPE.AUDIO)), [ dispatch ]);
const isLocalModerator = useSelector(isLocalParticipantModerator);
const isChatButtonEnabled = useSelector(isToolbarButtonEnabled('chat'));
const isParticipantVideoMuted = useSelector(getIsParticipantVideoMuted(p));
const kickRemoteParticipant = useCallback(() => {
dispatch(openDialog(KickRemoteParticipantDialog, {
participantID: p.id
}));
}, [ dispatch, p ]);
const muteAudio = useCallback(() => {
dispatch(openDialog(MuteRemoteParticipantDialog, {
participantID: p.id
}));
}, [ dispatch, p ]);
const muteEveryoneElse = useCallback(() => {
dispatch(openDialog(MuteEveryoneDialog, {
exclude: [ p.id ]
}));
}, [ dispatch, p ]);
const muteVideo = useCallback(() => {
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, {
participantID: p.id
}));
}, [ dispatch, p ]);
const sendPrivateMessage = useCallback(() => {
dispatch(openChat(p));
}, [ dispatch, p ]);
const { t } = useTranslation();
return (
@ -43,55 +77,85 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
<Avatar
className = 'participant-avatar'
participantId = { p.id }
size = { 32 } />
size = { 24 } />
<View style = { styles.contextMenuItemText }>
<Text style = { styles.contextMenuItemName }>
{ displayName }
</Text>
</View>
</View>
<TouchableOpacity
onPress = { muteAudio }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMicrophoneEmptySlash }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.mute') }</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { muteAudio }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMuteEveryoneElse }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.muteEveryoneElse') }</Text>
</TouchableOpacity>
<TouchableOpacity
style = { styles.contextMenuItemSection }>
<Icon
size = { 24 }
src = { IconVideoOff }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.stopVideo') }</Text>
</TouchableOpacity>
<TouchableOpacity
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconCloseCircle }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('videothumbnail.kick') }</Text>
</TouchableOpacity>
<TouchableOpacity
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMessage }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('toolbar.accessibilityLabel.privateMessage') }</Text>
</TouchableOpacity>
{
isLocalModerator
&& <TouchableOpacity
onPress = { muteAudio }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMicrophoneEmptySlash }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>
{ t('participantsPane.actions.mute') }
</Text>
</TouchableOpacity>
}
{
isLocalModerator
&& <TouchableOpacity
onPress = { muteEveryoneElse }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMuteEveryoneElse }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>
{ t('participantsPane.actions.muteEveryoneElse') }
</Text>
</TouchableOpacity>
}
{
isLocalModerator && (
isParticipantVideoMuted
|| <TouchableOpacity
onPress = { muteVideo }
style = { styles.contextMenuItemSection }>
<Icon
size = { 24 }
src = { IconVideoOff }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>
{ t('participantsPane.actions.stopVideo') }
</Text>
</TouchableOpacity>
)
}
{
isLocalModerator
&& <TouchableOpacity
onPress = { kickRemoteParticipant }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconCloseCircle }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>
{ t('videothumbnail.kick') }
</Text>
</TouchableOpacity>
}
{
isChatButtonEnabled
&& <TouchableOpacity
onPress = { sendPrivateMessage }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconMessage }
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>
{ t('toolbar.accessibilityLabel.privateMessage') }
</Text>
</TouchableOpacity>
}
<TouchableOpacity
style = { styles.contextMenuItemSection }>
<Icon
@ -100,6 +164,7 @@ export const ContextMenuMeetingParticipantDetails = ({ participant: p }: Props)
style = { styles.contextMenuItemIcon } />
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.networkStats') }</Text>
</TouchableOpacity>
<VolumeSlider />
</BottomSheet>
);
};

View File

@ -0,0 +1,32 @@
// @flow
import React from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteRemoteParticipantsVideoDialog
from '../AbstractMuteRemoteParticipantsVideoDialog';
/**
* Dialog to confirm a remote participant's video stop action.
*/
class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ConfirmDialog
contentKey = 'dialog.muteParticipantsVideoDialog'
onSubmit = { this._onSubmit } />
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(MuteRemoteParticipantsVideoDialog));

View File

@ -0,0 +1,119 @@
// @flow
import Slider from '@react-native-community/slider';
import React, { Component } from 'react';
import { View } from 'react-native';
import { withTheme } from 'react-native-paper';
import { Icon, IconVolumeEmpty } from '../../../base/icons';
import { VOLUME_SLIDER_SCALE } from '../../constants';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link VolumeSlider}.
*/
type Props = {
/**
* The value of the audio slider should display at when the component first
* mounts. Changes will be stored in state. The value should be a number
* between 0 and 1.
*/
initialValue: number,
/**
* The callback to invoke when the audio slider value changes.
*/
onChange: Function,
/**
* Theme used for styles.
*/
theme: Object
};
/**
* The type of the React {@code Component} state of {@link VolumeSlider}.
*/
type State = {
/**
* The volume of the participant's audio element. The value will
* be represented by a slider.
*/
volumeLevel: number
};
/**
* Component that renders the volume slider.
*
* @returns {React$Element<any>}
*/
class VolumeSlider extends Component<Props, State> {
/**
* Initializes a new {@code VolumeSlider} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
volumeLevel: (props.initialValue || 0) * VOLUME_SLIDER_SCALE
};
// Bind event handlers so they are only bound once for every instance.
this._onVolumeChange = this._onVolumeChange.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { volumeLevel } = this.state;
const { palette } = this.props.theme;
return (
<View style = { styles.volumeSliderContainer }>
<Icon
size = { 24 }
src = { IconVolumeEmpty }
style = { styles.volumeSliderIcon } />
<View style = { styles.sliderContainer }>
<Slider
maximumTrackTintColor = { palette.field02 }
maximumValue = { VOLUME_SLIDER_SCALE }
minimumTrackTintColor = { palette.action01 }
minimumValue = { 0 }
onValueChange = { this._onVolumeChange }
/* eslint-disable-next-line react-native/no-inline-styles */
value = { volumeLevel } />
</View>
</View>
);
}
_onVolumeChange: (Object) => void;
/**
* Sets the internal state of the volume level for the volume slider.
* Invokes the prop onVolumeChange to notify of volume changes.
*
* @param {number} volumeLevel - Selected volume on slider.
* @private
* @returns {void}
*/
_onVolumeChange(volumeLevel) {
this.setState({ volumeLevel });
}
}
export default withTheme(VolumeSlider);

View File

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

View File

@ -6,6 +6,7 @@ import {
MD_ITEM_MARGIN_PADDING
} from '../../../base/dialog';
import { ColorPalette, createStyleSheet } from '../../../base/styles';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export default createStyleSheet({
participantNameContainer: {
@ -48,5 +49,22 @@ export default createStyleSheet({
statsWrapper: {
marginVertical: 10
},
volumeSliderContainer: {
alignItems: 'center',
flexDirection: 'row',
marginBottom: BaseTheme.spacing[3],
marginTop: BaseTheme.spacing[3],
width: '100%'
},
volumeSliderIcon: {
marginLeft: BaseTheme.spacing[1]
},
sliderContainer: {
marginLeft: BaseTheme.spacing[3],
width: 342
}
});

View File

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

View File

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