convert video-list component

This commit is contained in:
Ramón Souza 2021-11-03 14:34:03 +00:00
parent 53575aaf98
commit b989fa1840
7 changed files with 319 additions and 357 deletions

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette'; import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general'; import { mdPaddingX } from '/imports/ui/stylesheets/styled-components/general';
import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints'; import { mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import { actionsBarHeight, navbarHeight } from '/import/ui/stylesheets/styled-components/general'; import { actionsBarHeight, navbarHeight } from '/imports/ui/stylesheets/styled-components/general';
import Button from '/imports/ui/components/button/component'; import Button from '/imports/ui/components/button/component';
const NextPageButton = styled(Button)` const NextPageButton = styled(Button)`

View File

@ -1,308 +0,0 @@
@import "/imports/ui/stylesheets/variables/breakpoints";
@import "/imports/ui/stylesheets/variables/placeholders";
@import "/imports/ui/components/media/styles";
.videoCanvas {
--cam-dropdown-width: 70%;
--audio-indicator-width: 1.12rem;
--audio-indicator-fs: 75%;
position: absolute;
width: 100%;
min-height: var(--video-height);
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.videoCanvasLRPosition {
flex-wrap: wrap;
align-content: center;
order: 0;
}
.videoList {
display: grid;
grid-auto-flow: dense;
grid-gap: 1px;
justify-content: center;
@include mq($medium-up) {
grid-gap: 2px;
}
}
.videoListItem {
display: flex;
overflow: hidden;
width: 100%;
max-height: 100%;
&.focused {
grid-column: 1 / span 2;
grid-row: 1 / span 2;
}
}
.mirroredVideo {
transform: scale(-1, 1);
}
.content {
position: relative;
display: flex;
min-width: 100%;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 5px solid var(--color-primary);
opacity: 0;
pointer-events: none;
:global(.animationsEnabled) & {
transition: opacity .1s;
}
}
&.talking::after {
opacity: 0.7;
}
}
%media-area {
position: relative;
height: 100%;
width: 100%;
object-fit: contain;
background-color: var(--color-black);
}
.cursorGrab{
cursor: grab;
}
.cursorGrabbing{
cursor: grabbing;
}
.videoContainer{
width: 100%;
height: 100%;
}
.connecting {
@extend %media-area;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
white-space: nowrap;
z-index: 1;
vertical-align: middle;
border-radius: 1px;
opacity: 1;
}
.loadingText {
@extend %text-elipsis;
color: var(--color-white);
font-size: 100%;
}
.media {
@extend %media-area;
}
.info {
position: absolute;
display: flex;
bottom: 5px;
left: 5px;
right: 5px;
justify-content: space-between;
align-items: center;
height: 1.25rem;
z-index: 2;
}
.dropdown,
.dropdownFireFox {
display: flex;
outline: none !important;
width: var(--cam-dropdown-width);
@include mq($medium-up) {
>[aria-expanded] {
padding: .25rem;
}
}
@include mq($landscape) {
button {
width: calc(100vw - 4rem);
margin-left: 1rem;
}
}
}
.dropdownFireFox {
max-width: 100%;
}
.dropdownTrigger,
.userName {
@extend %text-elipsis;
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: var(--color-off-white);
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
}
.noMenu {
padding: 0 .5rem 0 .5rem !important;
}
.dropdownTrigger {
cursor: pointer;
&::after {
content: "\203a";
position: absolute;
transform: rotate(90deg);
top: 45%;
width: 0;
line-height: 0;
right: .45rem;
}
}
.dropdownContent {
min-width: 8.5rem;
[dir="rtl"] & {
right: 2rem;
}
@include mq($small-only) {
height: 90%;
width: 100vw;
}
}
.dropdownList {
@include mq($medium-up) {
font-size: .86rem;
}
}
.hiddenDesktop {
display: none;
@include mq($small-only) {
display: block;
}
}
.muted,
.voice {
display: inline-block;
width: var(--audio-indicator-width);
height: var(--audio-indicator-width);
min-width: var(--audio-indicator-width);
min-height: var(--audio-indicator-width);
color: var(--color-white);
border-radius: 50%;
&::before {
font-size: var(--audio-indicator-fs);
}
}
.muted {
background-color: var(--color-danger);
}
.voice {
background-color: var(--color-success);
}
.nextPage,
.previousPage{
color: var(--color-white);
width: var(--md-padding-x);
i {
[dir="rtl"] & {
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
}
}
.nextPage {
margin-left: 1px;
@include mq($medium-up) {
margin-left: 2px;
}
}
.nextPageLRPosition {
order: 3;
margin-right: 2px;
}
.previousPage {
margin-right: 1px;
@include mq($medium-up) {
margin-right: 2px;
}
}
.previousPageLRPosition {
order: 2;
margin-left: 2px;
}
.unhealthyStream {
filter: grayscale(50%) opacity(50%);
}
.reconnecting {
@extend .connectingSpinner;
background-color: transparent;
color: var(--color-white);
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99;
}
.break {
order: 1;
flex-basis: 100%;
height: 5px;
}

View File

@ -2,12 +2,10 @@ import React, { Component } from 'react';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import cx from 'classnames';
import BBBMenu from '/imports/ui/components/menu/component'; import BBBMenu from '/imports/ui/components/menu/component';
import Icon from '/imports/ui/components/icon/component';
import FullscreenService from '/imports/ui/components/fullscreen-button/service'; import FullscreenService from '/imports/ui/components/fullscreen-button/service';
import FullscreenButtonContainer from '/imports/ui/components/fullscreen-button/container'; import FullscreenButtonContainer from '/imports/ui/components/fullscreen-button/container';
import { styles } from '../styles.scss'; import Styled from './styles';
import VideoService from '../../service'; import VideoService from '../../service';
import { import {
isStreamStateUnhealthy, isStreamStateUnhealthy,
@ -15,6 +13,7 @@ import {
unsubscribeFromStreamStateChange, unsubscribeFromStreamStateChange,
} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
import { ACTIONS } from '/imports/ui/components/layout/enums'; import { ACTIONS } from '/imports/ui/components/layout/enums';
import Settings from '/imports/ui/services/settings';
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen; const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
@ -188,64 +187,54 @@ class VideoListItem extends Component {
const shouldRenderReconnect = !isStreamHealthy && videoIsReady; const shouldRenderReconnect = !isStreamHealthy && videoIsReady;
const { isFirefox } = browserInfo; const { isFirefox } = browserInfo;
const { animations } = Settings.application;
return ( return (
<div <Styled.Content
talking={voiceUser.talking}
fullscreen={isFullscreenContext}
data-test={voiceUser.talking ? 'webcamItemTalkingUser' : 'webcamItem'} data-test={voiceUser.talking ? 'webcamItemTalkingUser' : 'webcamItem'}
className={cx({ animations={animations}
[styles.content]: true,
[styles.talking]: voiceUser.talking,
[styles.fullscreen]: isFullscreenContext,
})}
> >
{ {
!videoIsReady !videoIsReady
&& ( && (
<div <Styled.WebcamConnecting
data-test="webcamConnecting" data-test="webcamConnecting"
className={cx({ talking={voiceUser.talking}
[styles.connecting]: true, animations={animations}
[styles.content]: true,
[styles.talking]: voiceUser.talking,
})}
> >
<span className={styles.loadingText}>{name}</span> <Styled.LoadingText>{name}</Styled.LoadingText>
</div> </Styled.WebcamConnecting>
) )
} }
{ {
shouldRenderReconnect shouldRenderReconnect
&& <div className={styles.reconnecting} /> && <Styled.Reconnecting />
} }
<div <Styled.VideoContainer ref={(ref) => { this.videoContainer = ref; }}>
className={styles.videoContainer} <Styled.Video
ref={(ref) => { this.videoContainer = ref; }}
>
<video
muted muted
data-test={this.mirrorOwnWebcam ? 'mirroredVideoContainer' : 'videoContainer'} data-test={this.mirrorOwnWebcam ? 'mirroredVideoContainer' : 'videoContainer'}
className={cx({ mirrored={(this.mirrorOwnWebcam && !mirrored)
[styles.media]: true, || (!this.mirrorOwnWebcam && mirrored)}
[styles.mirroredVideo]: (this.mirrorOwnWebcam && !mirrored) unhealthyStream={shouldRenderReconnect}
|| (!this.mirrorOwnWebcam && mirrored),
[styles.unhealthyStream]: shouldRenderReconnect,
})}
ref={(ref) => { this.videoTag = ref; }} ref={(ref) => { this.videoTag = ref; }}
autoPlay autoPlay
playsInline playsInline
/> />
{videoIsReady && this.renderFullscreenButton()} {videoIsReady && this.renderFullscreenButton()}
</div> </Styled.VideoContainer>
{videoIsReady {videoIsReady
&& ( && (
<div className={styles.info}> <Styled.Info>
{enableVideoMenu && availableActions.length >= 1 {enableVideoMenu && availableActions.length >= 1
? ( ? (
<BBBMenu <BBBMenu
trigger={<div tabIndex={0} className={styles.dropdownTrigger}>{name}</div>} trigger={<Styled.DropdownTrigger tabIndex={0}>{name}</Styled.DropdownTrigger>}
actions={this.getAvailableActions()} actions={this.getAvailableActions()}
opts={{ opts={{
id: "default-dropdown-menu", id: "default-dropdown-menu",
@ -260,24 +249,18 @@ class VideoListItem extends Component {
/> />
) )
: ( : (
<div className={isFirefox ? styles.dropdownFireFox <Styled.Dropdown isFirefox={isFirefox}>
: styles.dropdown} <Styled.UserName noMenu={numOfStreams < 3}>
>
<span className={cx({
[styles.userName]: true,
[styles.noMenu]: numOfStreams < 3,
})}
>
{name} {name}
</span> </Styled.UserName>
</div> </Styled.Dropdown>
)} )}
{voiceUser.muted && !voiceUser.listenOnly ? <Icon className={styles.muted} iconName="unmute_filled" /> : null} {voiceUser.muted && !voiceUser.listenOnly ? <Styled.Muted iconName="unmute_filled" /> : null}
{voiceUser.listenOnly ? <Icon className={styles.voice} iconName="listen" /> : null} {voiceUser.listenOnly ? <Styled.Voice iconName="listen" /> : null}
{voiceUser.joined && !voiceUser.muted ? <Icon className={styles.voice} iconName="unmute" /> : null} {voiceUser.joined && !voiceUser.muted ? <Styled.Voice iconName="unmute" /> : null}
</div> </Styled.Info>
)} )}
</div> </Styled.Content>
); );
} }
} }

View File

@ -0,0 +1,278 @@
import styled from 'styled-components';
import Icon from '/imports/ui/components/icon/component';
import {
colorPrimary,
colorBlack,
colorWhite,
colorOffWhite,
colorDanger,
colorSuccess,
} from '/imports/ui/stylesheets/styled-components/palette';
import { TextElipsis, DivElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
import { landscape, mediumUp } from '/imports/ui/stylesheets/styled-components/breakpoints';
import {
audioIndicatorWidth,
audioIndicatorFs,
} from '/imports/ui/stylesheets/styled-components/general';
const Content = styled.div`
position: relative;
display: flex;
min-width: 100%;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 5px solid ${colorPrimary};
opacity: 0;
pointer-events: none;
${({ animations }) => animations && `
transition: opacity .1s;
`}
}
${({ talking }) => talking && `
&::after {
opacity: 0.7;
}
`}
${({ fullscreen }) => fullscreen && `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99;
`}
`;
const WebcamConnecting = styled.div`
position: relative;
height: 100%;
width: 100%;
object-fit: contain;
background-color: ${colorBlack};
display: flex;
justify-content: center;
align-items: center;
position: absolute;
white-space: nowrap;
z-index: 1;
vertical-align: middle;
border-radius: 1px;
opacity: 1;
position: relative;
display: flex;
min-width: 100%;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 5px solid ${colorPrimary};
opacity: 0;
pointer-events: none;
${({ animations }) => animations && `
transition: opacity .1s;
`}
}
${({ talking }) => talking && `
&::after {
opacity: 0.7;
}
`}
`;
const LoadingText = styled(TextElipsis)`
color: ${colorWhite};
font-size: 100%;
`;
const Reconnecting = styled.div`
position: absolute;
height: 100%;
width: 100%;
object-fit: contain;
font-size: 2.5rem;
text-align: center;
white-space: nowrap;
z-index: 1;
&::after {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
margin: 0 -0.25em 0 0;
[dir="rtl"] & {
margin: 0 0 0 -0.25em;
}
}
&::before {
content: "\\e949";
/* ascii code for the ellipsis character */
font-family: 'bbb-icons' !important;
display: inline-block;
${({ animations }) => animations && `
animation: spin 4s infinite linear;
`}
}
background-color: transparent;
color: ${colorWhite};
`;
const VideoContainer = styled.div`
width: 100%;
height: 100%;
`;
const Video = styled.video`
position: relative;
height: 100%;
width: 100%;
object-fit: contain;
background-color: ${colorBlack};
${({ mirrored }) => mirrored && `
transform: scale(-1, 1);
`}
${({ unhealthyStream }) => unhealthyStream && `
filter: grayscale(50%) opacity(50%);
`}
`;
const Info = styled.div`
position: absolute;
display: flex;
bottom: 5px;
left: 5px;
right: 5px;
justify-content: space-between;
align-items: center;
height: 1.25rem;
z-index: 2;
`;
const Dropdown = styled.div`
display: flex;
outline: none !important;
width: 70%;
@media ${mediumUp} {
>[aria-expanded] {
padding: .25rem;
}
}
@media ${landscape} {
button {
width: calc(100vw - 4rem);
margin-left: 1rem;
}
}
${({ isFirefox }) => isFirefox && `
max-width: 100%;
`}
`;
const UserName = styled(TextElipsis)`
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
${({ noMenu }) => noMenu && `
padding: 0 .5rem 0 .5rem !important;
`}
`;
const Muted = styled(Icon)`
display: inline-block;
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorDanger};
`;
const Voice = styled(Icon)`
display: inline-block;
width: ${audioIndicatorWidth};
height: ${audioIndicatorWidth};
min-width: ${audioIndicatorWidth};
min-height: ${audioIndicatorWidth};
color: ${colorWhite};
border-radius: 50%;
&::before {
font-size: ${audioIndicatorFs};
}
background-color: ${colorSuccess};
`;
const DropdownTrigger = styled(DivElipsis)`
position: relative;
// Keep the background with 0.5 opacity, but leave the text with 1
background-color: rgba(0, 0, 0, 0.5);
border-radius: 1px;
color: ${colorOffWhite};
padding: 0 1rem 0 .5rem !important;
font-size: 80%;
cursor: pointer;
&::after {
content: "\\203a";
position: absolute;
transform: rotate(90deg);
top: 45%;
width: 0;
line-height: 0;
right: .45rem;
}
`;
export default {
Content,
WebcamConnecting,
LoadingText,
Reconnecting,
VideoContainer,
Video,
Info,
Dropdown,
UserName,
Muted,
Voice,
DropdownTrigger,
};

View File

@ -1,9 +1,11 @@
const smallOnly = 'only screen and (max-width: 40em)'; const smallOnly = 'only screen and (max-width: 40em)';
const mediumOnly = 'only screen and (min-width: 40.063em) and (max-width: 64em)'; const mediumOnly = 'only screen and (min-width: 40.063em) and (max-width: 64em)';
const mediumUp = 'only screen and (min-width: 40.063em)'; const mediumUp = 'only screen and (min-width: 40.063em)';
const landscape = "only screen and (orientation: landscape)";
export { export {
smallOnly, smallOnly,
mediumOnly, mediumOnly,
mediumUp, mediumUp,
landscape,
}; };

View File

@ -22,6 +22,8 @@ const titlePositionLeft = '2.2rem';
const userIndicatorsOffset = '-5px'; const userIndicatorsOffset = '-5px';
const indicatorPadding = '.45rem'; // used to center presenter indicator icon in Chrome / Firefox / Edge const indicatorPadding = '.45rem'; // used to center presenter indicator icon in Chrome / Firefox / Edge
const actionsBarHeight = '75px'; // TODO: Change to ActionsBar real height const actionsBarHeight = '75px'; // TODO: Change to ActionsBar real height
const audioIndicatorWidth = '1.12rem';
const audioIndicatorFs = '75%';
export { export {
borderSizeSmall, borderSizeSmall,
@ -47,4 +49,6 @@ export {
userIndicatorsOffset, userIndicatorsOffset,
indicatorPadding, indicatorPadding,
actionsBarHeight, actionsBarHeight,
audioIndicatorWidth,
audioIndicatorFs,
}; };

View File

@ -1,6 +1,8 @@
const colorWhite = '#FFF'; const colorWhite = '#FFF';
const colorOffWhite = '#F3F6F9'; const colorOffWhite = '#F3F6F9';
const colorBlack = '#000000';
const colorGray = '#4E5A66'; const colorGray = '#4E5A66';
const colorGrayDark = '#06172A'; const colorGrayDark = '#06172A';
const colorGrayLight = '#8B9AA8'; const colorGrayLight = '#8B9AA8';
@ -39,6 +41,7 @@ const loaderBullet = colorWhite;
export { export {
colorWhite, colorWhite,
colorOffWhite, colorOffWhite,
colorBlack,
colorGray, colorGray,
colorGrayDark, colorGrayDark,
colorGrayLight, colorGrayLight,