feat(video-picker) Redesign (#12902)

Convert some files to TS
Implement redesign
Add Virtual background and Flip video to picker menu
This commit is contained in:
Robert Pintilii 2023-02-14 12:15:37 +02:00 committed by GitHub
parent 3cb0df579c
commit 27b8794d8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 157 additions and 113 deletions

View File

@ -4,7 +4,7 @@
&-content { &-content {
position: relative; position: relative;
right: auto; right: auto;
margin-bottom: 8px; margin-bottom: 4px;
max-height: 456px; max-height: 456px;
overflow: auto; overflow: auto;
width: 300px; width: 300px;

View File

@ -3,49 +3,38 @@
display: inline-block; display: inline-block;
&-container { &-container {
max-height: 344px; max-height: 456px;
background: $menuBG;
border-radius: 3px;
overflow: auto; overflow: auto;
padding: 8px; margin-bottom: 4px;
margin-bottom: 8px; position: relative;
right: auto;
} }
&-entry { &-entry {
cursor: pointer; cursor: pointer;
height: 168px; height: 138px;
margin-bottom: 8px; width: 244px;
position: relative; position: relative;
width: 284px; margin: 0 7px 4px;
border-radius: 6px;
box-sizing: border-box;
overflow: hidden;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
&--selected { &--selected {
border: 3px solid #31B76A; border: 2px solid #4687ED;
border-radius: 3px;
cursor: default;
height: 162px;
width: 278px;
} }
} }
&-video { &-video {
border-radius: 3px;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
} }
&-overlay {
background: rgba(42, 58, 75, 0.6);
height: 100%;
position: absolute;
width: 100%;
z-index: 1;
}
&-error { &-error {
align-items: center; align-items: center;
display: flex; display: flex;
@ -56,23 +45,22 @@
} }
&-label { &-label {
bottom: 8px;
color: #fff;
position: absolute; position: absolute;
width: 100%; bottom: 0;
left: 0;
right: 0;
max-width: 100%;
padding: 8px;
z-index: 2; z-index: 2;
&-container {
margin: 0 16px;
}
&-text { &-text {
background-color: #131519; background-color: rgba(0, 0, 0, 0.7);
border-radius: 3px; border-radius: 4px;
padding: 2px 8px; padding: 4px 8px;
font-size: 13px; color: #fff;
line-height: 20px; font-size: 12px;
margin: 0 auto; line-height: 16px;
font-weight: 600;
max-width: calc(100% - 16px); max-width: calc(100% - 16px);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -80,8 +68,8 @@
white-space: nowrap; white-space: nowrap;
} }
} }
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) { &-checkbox-container {
padding: 0; padding: 10px 14px;
} }
} }

View File

@ -1286,6 +1286,7 @@
"grantModerator": "Grant Moderator Rights", "grantModerator": "Grant Moderator Rights",
"hideSelfView": "Hide self view", "hideSelfView": "Hide self view",
"kick": "Kick out", "kick": "Kick out",
"mirrorVideo": "Mirror my video",
"moderator": "Moderator", "moderator": "Moderator",
"mute": "Participant is muted", "mute": "Participant is muted",
"muted": "Muted", "muted": "Muted",

View File

@ -1,5 +1,4 @@
// @flow // @ts-ignore
import Video from './web/Video'; import Video from './web/Video';
export default Video; export default Video;

View File

@ -34,6 +34,9 @@ const getComputedOuterHeight = (element: HTMLElement) => {
interface IProps { interface IProps {
/**
* ARIA attributes.
*/
[key: `aria-${string}`]: string; [key: `aria-${string}`]: string;
/** /**
@ -106,6 +109,11 @@ interface IProps {
*/ */
onMouseLeave?: (e?: React.MouseEvent) => void; onMouseLeave?: (e?: React.MouseEvent) => void;
/**
* Container role.
*/
role?: string;
/** /**
* Tab index for the menu. * Tab index for the menu.
*/ */
@ -167,7 +175,9 @@ const ContextMenu = ({
onDrawerClose, onDrawerClose,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
tabIndex role,
tabIndex,
...aria
}: IProps) => { }: IProps) => {
const [ isHidden, setIsHidden ] = useState(true); const [ isHidden, setIsHidden ] = useState(true);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@ -225,6 +235,7 @@ const ContextMenu = ({
</Drawer> </Drawer>
</JitsiPortal> </JitsiPortal>
: <div : <div
{ ...aria }
aria-label = { accessibilityLabel } aria-label = { accessibilityLabel }
className = { cx(participantsPaneTheme.ignoredChildClassName, className = { cx(participantsPaneTheme.ignoredChildClassName,
styles.contextMenu, styles.contextMenu,
@ -237,7 +248,7 @@ const ContextMenu = ({
onMouseEnter = { onMouseEnter } onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave } onMouseLeave = { onMouseLeave }
ref = { containerRef } ref = { containerRef }
role = 'menu' role = { role ?? 'menu' }
tabIndex = { tabIndex }> tabIndex = { tabIndex }>
{children} {children}
</div>; </div>;

View File

@ -1,45 +1,63 @@
// @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { translate } from '../../../../base/i18n'; import { IReduxState, IStore } from '../../../../app/types';
import Video from '../../../../base/media/components/Video'; import { openDialog } from '../../../../base/dialog/actions';
import { equals } from '../../../../base/redux'; import { translate } from '../../../../base/i18n/functions';
import { createLocalVideoTracks } from '../../../functions'; import { IconImage } from '../../../../base/icons/svg';
import Video from '../../../../base/media/components/Video.web';
import { equals } from '../../../../base/redux/functions';
import { updateSettings } from '../../../../base/settings/actions';
import Checkbox from '../../../../base/ui/components/web/Checkbox';
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
import { createLocalVideoTracks } from '../../../functions.web';
const videoClassName = 'video-preview-video flipVideoX'; const videoClassName = 'video-preview-video flipVideoX';
/** /**
* The type of the React {@code Component} props of {@link VideoSettingsContent}. * The type of the React {@code Component} props of {@link VideoSettingsContent}.
*/ */
export type Props = { export interface IProps extends WithTranslation {
/**
* Callback to change the flip state.
*/
changeFlip: (flip: boolean) => void;
/** /**
* The deviceId of the camera device currently being used. * The deviceId of the camera device currently being used.
*/ */
currentCameraDeviceId: string, currentCameraDeviceId: string;
/**
* Whether or not the local video is flipped.
*/
localFlipX: boolean;
/**
* Open virtual background dialog.
*/
selectBackground: () => void;
/** /**
* Callback invoked to change current camera. * Callback invoked to change current camera.
*/ */
setVideoInputDevice: Function, setVideoInputDevice: Function;
/**
* Invoked to obtain translated strings.
*/
t: Function,
/** /**
* Callback invoked to toggle the settings popup visibility. * Callback invoked to toggle the settings popup visibility.
*/ */
toggleVideoSettings: Function, toggleVideoSettings: Function;
/** /**
* All the camera device ids currently connected. * All the camera device ids currently connected.
*/ */
videoDeviceIds: string[], videoDeviceIds: string[];
}; }
/** /**
* The type of the React {@code Component} state of {@link VideoSettingsContent}. * The type of the React {@code Component} state of {@link VideoSettingsContent}.
@ -49,7 +67,7 @@ type State = {
/** /**
* An array of all the jitsiTracks and eventual errors. * An array of all the jitsiTracks and eventual errors.
*/ */
trackData: Object[], trackData: { deviceId: string; error?: string; jitsiTrack: any | null; }[];
}; };
/** /**
@ -58,9 +76,8 @@ type State = {
* *
* @augments Component * @augments Component
*/ */
class VideoSettingsContent extends Component<Props, State> { class VideoSettingsContent extends Component<IProps, State> {
_componentWasUnmounted: boolean; _componentWasUnmounted: boolean;
_videoContentRef: Object;
/** /**
* Initializes a new {@code VideoSettingsContent} instance. * Initializes a new {@code VideoSettingsContent} instance.
@ -68,10 +85,9 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {Object} props - The read-only properties with which the new * @param {Object} props - The read-only properties with which the new
* instance is to be initialized. * instance is to be initialized.
*/ */
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._onEscClick = this._onEscClick.bind(this); this._onToggleFlip = this._onToggleFlip.bind(this);
this._videoContentRef = React.createRef();
this.state = { this.state = {
trackData: new Array(props.videoDeviceIds.length).fill({ trackData: new Array(props.videoDeviceIds.length).fill({
@ -79,20 +95,16 @@ class VideoSettingsContent extends Component<Props, State> {
}) })
}; };
} }
_onEscClick: (KeyboardEvent) => void;
/** /**
* Click handler for the video entries. * Toggles local video flip state.
* *
* @param {KeyboardEvent} event - Esc key click to close the popup.
* @returns {void} * @returns {void}
*/ */
_onEscClick(event) { _onToggleFlip() {
if (event.key === 'Escape') { const { localFlipX, changeFlip } = this.props;
event.preventDefault();
event.stopPropagation(); changeFlip(!localFlipX);
this._videoContentRef.current.style.display = 'none';
}
} }
/** /**
@ -122,9 +134,9 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {Object[]} trackData - An array of tracks that are to be disposed. * @param {Object[]} trackData - An array of tracks that are to be disposed.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
_disposeTracks(trackData) { _disposeTracks(trackData: { jitsiTrack: any; }[]) {
trackData.forEach(({ jitsiTrack }) => { trackData.forEach(({ jitsiTrack }) => {
jitsiTrack && jitsiTrack.dispose(); jitsiTrack?.dispose();
}); });
} }
@ -134,7 +146,7 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {string} deviceId - The id of the camera device. * @param {string} deviceId - The id of the camera device.
* @returns {Function} * @returns {Function}
*/ */
_onEntryClick(deviceId) { _onEntryClick(deviceId: string) {
return () => { return () => {
this.props.setVideoInputDevice(deviceId); this.props.setVideoInputDevice(deviceId);
this.props.toggleVideoSettings(); this.props.toggleVideoSettings();
@ -148,7 +160,7 @@ class VideoSettingsContent extends Component<Props, State> {
* @param {number} index - The index of the entry. * @param {number} index - The index of the entry.
* @returns {React$Node} * @returns {React$Node}
*/ */
_renderPreviewEntry(data, index) { _renderPreviewEntry(data: { deviceId: string; error?: string; jitsiTrack: any | null; }, index: number) {
const { error, jitsiTrack, deviceId } = data; const { error, jitsiTrack, deviceId } = data;
const { currentCameraDeviceId, t } = this.props; const { currentCameraDeviceId, t } = this.props;
const isSelected = deviceId === currentCameraDeviceId; const isSelected = deviceId === currentCameraDeviceId;
@ -167,19 +179,19 @@ class VideoSettingsContent extends Component<Props, State> {
); );
} }
const props: Object = { const props: any = {
className, className,
key, key,
tabIndex tabIndex
}; };
const label = jitsiTrack && jitsiTrack.getTrackLabel(); const label = jitsiTrack?.getTrackLabel();
if (isSelected) { if (isSelected) {
props['aria-checked'] = true; props['aria-checked'] = true;
props.className = `${className} video-preview-entry--selected`; props.className = `${className} video-preview-entry--selected`;
} else { } else {
props.onClick = this._onEntryClick(deviceId); props.onClick = this._onEntryClick(deviceId);
props.onKeyPress = e => { props.onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
props.onClick(); props.onClick();
@ -192,12 +204,10 @@ class VideoSettingsContent extends Component<Props, State> {
{ ...props } { ...props }
role = 'radio'> role = 'radio'>
<div className = 'video-preview-label'> <div className = 'video-preview-label'>
{label && <div className = 'video-preview-label-container'> {label && <div className = 'video-preview-label-text'>
<div className = 'video-preview-label-text'> <span>{label}</span>
<span>{label}</span></div>
</div>} </div>}
</div> </div>
<div className = 'video-preview-overlay' />
<Video <Video
className = { videoClassName } className = { videoClassName }
playsinline = { true } playsinline = { true }
@ -230,7 +240,7 @@ class VideoSettingsContent extends Component<Props, State> {
* *
* @inheritdoc * @inheritdoc
*/ */
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: IProps) {
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) { if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
this._setTracks(); this._setTracks();
} }
@ -243,21 +253,57 @@ class VideoSettingsContent extends Component<Props, State> {
*/ */
render() { render() {
const { trackData } = this.state; const { trackData } = this.state;
const { selectBackground, t, localFlipX } = this.props;
return ( return (
<div <ContextMenu
aria-labelledby = 'video-settings-button' aria-labelledby = 'video-settings-button'
className = 'video-preview-container' className = 'video-preview-container'
hidden = { false }
id = 'video-settings-dialog' id = 'video-settings-dialog'
onKeyDown = { this._onEscClick }
ref = { this._videoContentRef }
role = 'radiogroup' role = 'radiogroup'
tabIndex = '-1'> tabIndex = { -1 }>
{trackData.map((data, i) => this._renderPreviewEntry(data, i))} <ContextMenuItemGroup>
</div> {trackData.map((data, i) => this._renderPreviewEntry(data, i))}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
<ContextMenuItem
accessibilityLabel = 'virtualBackground.title'
icon = { IconImage }
onClick = { selectBackground }
text = { t('virtualBackground.title') } />
<div
className = 'video-preview-checkbox-container'
// eslint-disable-next-line react/jsx-no-bind
onClick = { e => e.stopPropagation() }>
<Checkbox
checked = { localFlipX }
label = { t('videothumbnail.mirrorVideo') }
onChange = { this._onToggleFlip } />
</div>
</ContextMenuItemGroup>
</ContextMenu>
); );
} }
} }
const mapStateToProps = (state: IReduxState) => {
const { localFlipX } = state['features/base/settings'];
export default translate(VideoSettingsContent); return {
localFlipX: Boolean(localFlipX)
};
};
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
changeFlip: (flip: boolean) => {
dispatch(updateSettings({
localFlipX: flip
}));
}
};
};
export default translate(connect(mapStateToProps, mapDispatchToProps)(VideoSettingsContent));

View File

@ -1,7 +1,7 @@
// @flow import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
import React from 'react';
import { IReduxState } from '../../../../app/types';
import { import {
setVideoInputDeviceAndUpdateSettings setVideoInputDeviceAndUpdateSettings
} from '../../../../base/devices/actions.web'; } from '../../../../base/devices/actions.web';
@ -9,36 +9,35 @@ import {
getVideoDeviceIds getVideoDeviceIds
} from '../../../../base/devices/functions.web'; } from '../../../../base/devices/functions.web';
import Popover from '../../../../base/popover/components/Popover.web'; import Popover from '../../../../base/popover/components/Popover.web';
import { connect } from '../../../../base/redux';
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants'; import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
import { getCurrentCameraDeviceId } from '../../../../base/settings'; import { getCurrentCameraDeviceId } from '../../../../base/settings/functions.web';
import { toggleVideoSettings } from '../../../actions'; import { toggleVideoSettings } from '../../../actions';
import { getVideoSettingsVisibility } from '../../../functions'; import { getVideoSettingsVisibility } from '../../../functions.web';
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent'; import VideoSettingsContent, { type IProps as VideoSettingsProps } from './VideoSettingsContent';
type Props = VideoSettingsProps & { interface IProps extends VideoSettingsProps {
/** /**
* Component children (the Video button). * Component children (the Video button).
*/ */
children: React$Node, children: ReactNode;
/** /**
* Flag controlling the visibility of the popup. * Flag controlling the visibility of the popup.
*/ */
isOpen: boolean, isOpen: boolean;
/** /**
* Callback executed when the popup closes. * Callback executed when the popup closes.
*/ */
onClose: Function, onClose: Function;
/** /**
* The popup placement enum value. * The popup placement enum value.
*/ */
popupPlacement: string popupPlacement: string;
} }
/** /**
@ -54,7 +53,7 @@ function VideoSettingsPopup({
popupPlacement, popupPlacement,
setVideoInputDevice, setVideoInputDevice,
videoDeviceIds videoDeviceIds
}: Props) { }: IProps) {
return ( return (
<div className = 'video-preview'> <div className = 'video-preview'>
<Popover <Popover
@ -80,14 +79,14 @@ function VideoSettingsPopup({
* @param {Object} state - Redux state. * @param {Object} state - Redux state.
* @returns {Object} * @returns {Object}
*/ */
function mapStateToProps(state) { function mapStateToProps(state: IReduxState) {
const { clientWidth } = state['features/base/responsive-ui']; const { clientWidth } = state['features/base/responsive-ui'];
return { return {
currentCameraDeviceId: getCurrentCameraDeviceId(state), currentCameraDeviceId: getCurrentCameraDeviceId(state),
isOpen: getVideoSettingsVisibility(state), isOpen: Boolean(getVideoSettingsVisibility(state)),
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end', popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
videoDeviceIds: getVideoDeviceIds(state) videoDeviceIds: getVideoDeviceIds(state) ?? []
}; };
} }

View File

@ -377,7 +377,7 @@ const styles = () => {
rowGap: '8px', rowGap: '8px',
margin: 0, margin: 0,
padding: '16px', padding: '16px',
marginBottom: '8px' marginBottom: '4px'
} }
}; };
}; };