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:
parent
3cb0df579c
commit
27b8794d8c
|
@ -4,7 +4,7 @@
|
|||
&-content {
|
||||
position: relative;
|
||||
right: auto;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
|
|
|
@ -3,49 +3,38 @@
|
|||
display: inline-block;
|
||||
|
||||
&-container {
|
||||
max-height: 344px;
|
||||
background: $menuBG;
|
||||
border-radius: 3px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&-entry {
|
||||
cursor: pointer;
|
||||
height: 168px;
|
||||
margin-bottom: 8px;
|
||||
height: 138px;
|
||||
width: 244px;
|
||||
position: relative;
|
||||
width: 284px;
|
||||
margin: 0 7px 4px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
border: 3px solid #31B76A;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
height: 162px;
|
||||
width: 278px;
|
||||
border: 2px solid #4687ED;
|
||||
}
|
||||
}
|
||||
|
||||
&-video {
|
||||
border-radius: 3px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
background: rgba(42, 58, 75, 0.6);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-error {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -56,23 +45,22 @@
|
|||
}
|
||||
|
||||
&-label {
|
||||
bottom: 8px;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 100%;
|
||||
padding: 8px;
|
||||
z-index: 2;
|
||||
|
||||
&-container {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
background-color: #131519;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
max-width: calc(100% - 16px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -80,8 +68,8 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div:nth-child(2) {
|
||||
padding: 0;
|
||||
|
||||
&-checkbox-container {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1286,6 +1286,7 @@
|
|||
"grantModerator": "Grant Moderator Rights",
|
||||
"hideSelfView": "Hide self view",
|
||||
"kick": "Kick out",
|
||||
"mirrorVideo": "Mirror my video",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
|
||||
// @ts-ignore
|
||||
import Video from './web/Video';
|
||||
|
||||
export default Video;
|
|
@ -34,6 +34,9 @@ const getComputedOuterHeight = (element: HTMLElement) => {
|
|||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* ARIA attributes.
|
||||
*/
|
||||
[key: `aria-${string}`]: string;
|
||||
|
||||
/**
|
||||
|
@ -106,6 +109,11 @@ interface IProps {
|
|||
*/
|
||||
onMouseLeave?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Container role.
|
||||
*/
|
||||
role?: string;
|
||||
|
||||
/**
|
||||
* Tab index for the menu.
|
||||
*/
|
||||
|
@ -167,7 +175,9 @@ const ContextMenu = ({
|
|||
onDrawerClose,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
tabIndex
|
||||
role,
|
||||
tabIndex,
|
||||
...aria
|
||||
}: IProps) => {
|
||||
const [ isHidden, setIsHidden ] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
@ -225,6 +235,7 @@ const ContextMenu = ({
|
|||
</Drawer>
|
||||
</JitsiPortal>
|
||||
: <div
|
||||
{ ...aria }
|
||||
aria-label = { accessibilityLabel }
|
||||
className = { cx(participantsPaneTheme.ignoredChildClassName,
|
||||
styles.contextMenu,
|
||||
|
@ -237,7 +248,7 @@ const ContextMenu = ({
|
|||
onMouseEnter = { onMouseEnter }
|
||||
onMouseLeave = { onMouseLeave }
|
||||
ref = { containerRef }
|
||||
role = 'menu'
|
||||
role = { role ?? 'menu' }
|
||||
tabIndex = { tabIndex }>
|
||||
{children}
|
||||
</div>;
|
||||
|
|
|
@ -1,45 +1,63 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import Video from '../../../../base/media/components/Video';
|
||||
import { equals } from '../../../../base/redux';
|
||||
import { createLocalVideoTracks } from '../../../functions';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/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';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
setVideoInputDevice: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
setVideoInputDevice: Function;
|
||||
|
||||
/**
|
||||
* Callback invoked to toggle the settings popup visibility.
|
||||
*/
|
||||
toggleVideoSettings: Function,
|
||||
toggleVideoSettings: Function;
|
||||
|
||||
/**
|
||||
* All the camera device ids currently connected.
|
||||
*/
|
||||
videoDeviceIds: string[],
|
||||
};
|
||||
videoDeviceIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
trackData: Object[],
|
||||
trackData: { deviceId: string; error?: string; jitsiTrack: any | null; }[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -58,9 +76,8 @@ type State = {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VideoSettingsContent extends Component<Props, State> {
|
||||
class VideoSettingsContent extends Component<IProps, State> {
|
||||
_componentWasUnmounted: boolean;
|
||||
_videoContentRef: Object;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this._onEscClick = this._onEscClick.bind(this);
|
||||
this._videoContentRef = React.createRef();
|
||||
this._onToggleFlip = this._onToggleFlip.bind(this);
|
||||
|
||||
this.state = {
|
||||
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}
|
||||
*/
|
||||
_onEscClick(event) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._videoContentRef.current.style.display = 'none';
|
||||
}
|
||||
_onToggleFlip() {
|
||||
const { localFlipX, changeFlip } = this.props;
|
||||
|
||||
changeFlip(!localFlipX);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,9 +134,9 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
* @param {Object[]} trackData - An array of tracks that are to be disposed.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_disposeTracks(trackData) {
|
||||
_disposeTracks(trackData: { jitsiTrack: any; }[]) {
|
||||
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.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onEntryClick(deviceId) {
|
||||
_onEntryClick(deviceId: string) {
|
||||
return () => {
|
||||
this.props.setVideoInputDevice(deviceId);
|
||||
this.props.toggleVideoSettings();
|
||||
|
@ -148,7 +160,7 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
* @param {number} index - The index of the entry.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderPreviewEntry(data, index) {
|
||||
_renderPreviewEntry(data: { deviceId: string; error?: string; jitsiTrack: any | null; }, index: number) {
|
||||
const { error, jitsiTrack, deviceId } = data;
|
||||
const { currentCameraDeviceId, t } = this.props;
|
||||
const isSelected = deviceId === currentCameraDeviceId;
|
||||
|
@ -167,19 +179,19 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
const props: Object = {
|
||||
const props: any = {
|
||||
className,
|
||||
key,
|
||||
tabIndex
|
||||
};
|
||||
const label = jitsiTrack && jitsiTrack.getTrackLabel();
|
||||
const label = jitsiTrack?.getTrackLabel();
|
||||
|
||||
if (isSelected) {
|
||||
props['aria-checked'] = true;
|
||||
props.className = `${className} video-preview-entry--selected`;
|
||||
} else {
|
||||
props.onClick = this._onEntryClick(deviceId);
|
||||
props.onKeyPress = e => {
|
||||
props.onKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
props.onClick();
|
||||
|
@ -192,12 +204,10 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
{ ...props }
|
||||
role = 'radio'>
|
||||
<div className = 'video-preview-label'>
|
||||
{label && <div className = 'video-preview-label-container'>
|
||||
<div className = 'video-preview-label-text'>
|
||||
<span>{label}</span></div>
|
||||
{label && <div className = 'video-preview-label-text'>
|
||||
<span>{label}</span>
|
||||
</div>}
|
||||
</div>
|
||||
<div className = 'video-preview-overlay' />
|
||||
<Video
|
||||
className = { videoClassName }
|
||||
playsinline = { true }
|
||||
|
@ -230,7 +240,7 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
|
||||
this._setTracks();
|
||||
}
|
||||
|
@ -243,21 +253,57 @@ class VideoSettingsContent extends Component<Props, State> {
|
|||
*/
|
||||
render() {
|
||||
const { trackData } = this.state;
|
||||
const { selectBackground, t, localFlipX } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
<ContextMenu
|
||||
aria-labelledby = 'video-settings-button'
|
||||
className = 'video-preview-container'
|
||||
hidden = { false }
|
||||
id = 'video-settings-dialog'
|
||||
onKeyDown = { this._onEscClick }
|
||||
ref = { this._videoContentRef }
|
||||
role = 'radiogroup'
|
||||
tabIndex = '-1'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
</div>
|
||||
tabIndex = { -1 }>
|
||||
<ContextMenuItemGroup>
|
||||
{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));
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import {
|
||||
setVideoInputDeviceAndUpdateSettings
|
||||
} from '../../../../base/devices/actions.web';
|
||||
|
@ -9,36 +9,35 @@ import {
|
|||
getVideoDeviceIds
|
||||
} from '../../../../base/devices/functions.web';
|
||||
import Popover from '../../../../base/popover/components/Popover.web';
|
||||
import { connect } from '../../../../base/redux';
|
||||
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 { 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).
|
||||
*/
|
||||
children: React$Node,
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* Callback executed when the popup closes.
|
||||
*/
|
||||
onClose: Function,
|
||||
onClose: Function;
|
||||
|
||||
/**
|
||||
* The popup placement enum value.
|
||||
*/
|
||||
popupPlacement: string
|
||||
popupPlacement: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,7 +53,7 @@ function VideoSettingsPopup({
|
|||
popupPlacement,
|
||||
setVideoInputDevice,
|
||||
videoDeviceIds
|
||||
}: Props) {
|
||||
}: IProps) {
|
||||
return (
|
||||
<div className = 'video-preview'>
|
||||
<Popover
|
||||
|
@ -80,14 +79,14 @@ function VideoSettingsPopup({
|
|||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
currentCameraDeviceId: getCurrentCameraDeviceId(state),
|
||||
isOpen: getVideoSettingsVisibility(state),
|
||||
popupPlacement: clientWidth <= SMALL_MOBILE_WIDTH ? 'auto' : 'top-end',
|
||||
videoDeviceIds: getVideoDeviceIds(state)
|
||||
isOpen: Boolean(getVideoSettingsVisibility(state)),
|
||||
popupPlacement: clientWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
|
||||
videoDeviceIds: getVideoDeviceIds(state) ?? []
|
||||
};
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ const styles = () => {
|
|||
rowGap: '8px',
|
||||
margin: 0,
|
||||
padding: '16px',
|
||||
marginBottom: '8px'
|
||||
marginBottom: '4px'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue