feat(native-participants-pane) added action dialogs for context menu participant details and native community slider
This commit is contained in:
parent
0b3991d9e1
commit
8d4cf7165e
|
@ -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?",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
{
|
||||
isLocalModerator
|
||||
&& <TouchableOpacity
|
||||
onPress = { muteAudio }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconMicrophoneEmptySlash }
|
||||
style = { styles.contextMenuItemIcon } />
|
||||
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.mute') }</Text>
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.mute') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress = { muteAudio }
|
||||
}
|
||||
{
|
||||
isLocalModerator
|
||||
&& <TouchableOpacity
|
||||
onPress = { muteEveryoneElse }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconMuteEveryoneElse }
|
||||
style = { styles.contextMenuItemIcon } />
|
||||
<Text style = { styles.contextMenuItemText }>{ t('participantsPane.actions.muteEveryoneElse') }</Text>
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.muteEveryoneElse') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<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>
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('participantsPane.actions.stopVideo') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
)
|
||||
}
|
||||
{
|
||||
isLocalModerator
|
||||
&& <TouchableOpacity
|
||||
onPress = { kickRemoteParticipant }
|
||||
style = { styles.contextMenuItem }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconCloseCircle }
|
||||
style = { styles.contextMenuItemIcon } />
|
||||
<Text style = { styles.contextMenuItemText }>{ t('videothumbnail.kick') }</Text>
|
||||
<Text style = { styles.contextMenuItemText }>
|
||||
{ t('videothumbnail.kick') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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));
|
|
@ -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);
|
||||
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue