Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add "Grid View" layout #17883

Merged
merged 6 commits into from May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions bigbluebutton-html5/imports/ui/components/app/component.jsx
Expand Up @@ -24,7 +24,7 @@ import UploaderContainer from '/imports/ui/components/presentation/presentation-
import CaptionsSpeechContainer from '/imports/ui/components/captions/speech/container';
import RandomUserSelectContainer from '/imports/ui/components/common/modal/random-user/container';
import ScreenReaderAlertContainer from '../screenreader-alert/container';
import NewWebcamContainer from '../webcam/container';
import WebcamContainer from '../webcam/container';
import PresentationAreaContainer from '../presentation/presentation-area/container';
import ScreenshareContainer from '../screenshare/container';
import ExternalVideoContainer from '../external-video-player/container';
Expand Down Expand Up @@ -556,7 +556,7 @@ class App extends Component {
<SidebarNavigationContainer />
<SidebarContentContainer isSharedNotesPinned={shouldShowSharedNotes} />
<NavBarContainer main="new" />
<NewWebcamContainer isLayoutSwapped={!presentationIsOpen} />
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
<Styled.TextMeasure id="text-measure" />
{shouldShowPresentation ? <PresentationAreaContainer darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} /> : null}
{shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} /> : null}
Expand Down
Expand Up @@ -55,7 +55,7 @@ const LayoutEngine = ({ layoutType }) => {

const cameraDockBounds = {};

if (cameraDockInput.numCameras === 0) {
if (cameraDockInput.numCameras === 0 && layoutType !== LAYOUT_TYPE.VIDEO_FOCUS) {
cameraDockBounds.width = 0;
cameraDockBounds.height = 0;

Expand Down
Expand Up @@ -176,7 +176,7 @@ const VideoFocusLayout = (props) => {
height = windowHeight() - DEFAULT_VALUES.navBarHeight - bannerAreaHeight();
minHeight = height;
maxHeight = height;
} else if (cameraDockInput.numCameras > 0 && isOpen && !isGeneralMediaOff) {
} else if (isOpen && !isGeneralMediaOff) {
if (sidebarContentInput.height > 0 && sidebarContentInput.height < windowHeight()) {
height = sidebarContentInput.height - bannerAreaHeight();
} else {
Expand Down Expand Up @@ -254,7 +254,6 @@ const VideoFocusLayout = (props) => {
) => {
const mediaBounds = {};
const { element: fullscreenElement } = fullscreen;
const sidebarSize = sidebarNavWidth + sidebarContentWidth;

if (fullscreenElement === 'Presentation' || fullscreenElement === 'Screenshare' || fullscreenElement === 'ExternalVideo') {
mediaBounds.width = windowWidth();
Expand All @@ -271,7 +270,7 @@ const VideoFocusLayout = (props) => {
mediaBounds.left = mediaAreaBounds.left;
mediaBounds.top = mediaAreaBounds.top + cameraDockBounds.height;
mediaBounds.width = mediaAreaBounds.width;
} else if (cameraDockInput.numCameras > 0 && presentationInput.isOpen) {
} else if (presentationInput.isOpen) {
mediaBounds.height = windowHeight() - sidebarContentHeight - bannerAreaHeight();
mediaBounds.left = !isRTL ? sidebarNavWidth : 0;
mediaBounds.right = isRTL ? sidebarNavWidth : 0;
Expand All @@ -283,13 +282,6 @@ const VideoFocusLayout = (props) => {
mediaBounds.height = 0;
mediaBounds.top = 0;
mediaBounds.left = 0;
} else {
mediaBounds.height = mediaAreaBounds.height;
mediaBounds.width = mediaAreaBounds.width;
mediaBounds.top = DEFAULT_VALUES.navBarHeight + bannerAreaHeight();
mediaBounds.left = !isRTL ? mediaAreaBounds.left : null;
mediaBounds.right = isRTL ? sidebarSize : null;
mediaBounds.zIndex = 1;
}

return mediaBounds;
Expand Down Expand Up @@ -329,7 +321,6 @@ const VideoFocusLayout = (props) => {
sidebarContentWidth.width,
sidebarContentHeight.height,
);
const isBottomResizable = cameraDockInput.numCameras > 0;

layoutContextDispatch({
type: ACTIONS.SET_NAVBAR_OUTPUT,
Expand Down Expand Up @@ -420,7 +411,7 @@ const VideoFocusLayout = (props) => {
value: {
top: false,
right: !isRTL,
bottom: isBottomResizable,
bottom: true,
left: isRTL,
},
});
Expand All @@ -436,7 +427,7 @@ const VideoFocusLayout = (props) => {
layoutContextDispatch({
type: ACTIONS.SET_CAMERA_DOCK_OUTPUT,
value: {
display: cameraDockInput.numCameras > 0,
display: true,
minWidth: cameraDockBounds.minWidth,
width: cameraDockBounds.width,
maxWidth: cameraDockBounds.maxWidth,
Expand Down
Expand Up @@ -1249,6 +1249,7 @@ class VideoProvider extends Component {
cameraDockBounds,
focusedId,
handleVideoFocus,
isGridEnabled,
} = this.props;

return (
Expand All @@ -1260,6 +1261,7 @@ class VideoProvider extends Component {
cameraDockBounds,
focusedId,
handleVideoFocus,
isGridEnabled,
}}
onVideoItemMount={this.createVideoTag}
onVideoItemUnmount={this.destroyVideoTag}
Expand Down
Expand Up @@ -4,8 +4,8 @@ import VideoProvider from './component';
import VideoService from './service';

const VideoProviderContainer = ({ children, ...props }) => {
const { streams } = props;
return (!streams.length ? null : <VideoProvider {...props}>{children}</VideoProvider>);
const { streams, isGridEnabled } = props;
return (!streams.length && !isGridEnabled ? null : <VideoProvider {...props}>{children}</VideoProvider>);
};

export default withTracker(({ swapLayout, ...rest }) => {
Expand Down
Expand Up @@ -95,12 +95,13 @@ class VideoList extends Component {
}

componentDidUpdate(prevProps) {
const { layoutType, cameraDock, streams, focusedId } = this.props;
const { layoutType, cameraDock, streams, focusedId, isGridEnabled, users } = this.props;
const { width: cameraDockWidth, height: cameraDockHeight } = cameraDock;
const {
layoutType: prevLayoutType,
cameraDock: prevCameraDock,
streams: prevStreams,
users: prevUsers,
focusedId: prevFocusedId,
} = prevProps;
const { width: prevCameraDockWidth, height: prevCameraDockHeight } = prevCameraDock;
Expand All @@ -111,6 +112,7 @@ class VideoList extends Component {
|| focusedId !== prevFocusedId
|| cameraDockWidth !== prevCameraDockWidth
|| cameraDockHeight !== prevCameraDockHeight
|| (isGridEnabled && users?.length !== prevUsers?.length)
|| streams.length !== prevStreams.length) {
this.handleCanvasResize();
}
Expand Down Expand Up @@ -177,8 +179,15 @@ class VideoList extends Component {
streams,
cameraDock,
layoutContextDispatch,
isGridEnabled,
users,
} = this.props;
let numItems = streams.length;

if (isGridEnabled) {
numItems += users.length;
}

if (numItems < 1 || !this.canvas || !this.grid) {
return;
}
Expand Down Expand Up @@ -298,10 +307,37 @@ class VideoList extends Component {
swapLayout,
handleVideoFocus,
focusedId,
users,
} = this.props;
const numOfStreams = streams.length;

return streams.map((vs) => {
const userItems = users ? users.map((user) => {
const { userId, name } = user;

return (
<Styled.VideoListItem
key={userId}
focused={false}
data-test="webcamVideoItem"
>
<VideoListItemContainer
numOfStreams={numOfStreams}
cameraId={userId}
userId={userId}
name={name}
focused={false}
isStream={false}
onVideoItemMount={() => {
this.handleCanvasResize();
}}
onVideoItemUnmount={onVideoItemUnmount}
swapLayout={swapLayout}
/>
</Styled.VideoListItem>
);
}) : null;

const videoItems = streams.map((vs) => {
const { stream, userId, name } = vs;
const isFocused = focusedId === stream && numOfStreams > 2;

Expand All @@ -317,6 +353,7 @@ class VideoList extends Component {
userId={userId}
name={name}
focused={isFocused}
isStream={true}
onHandleVideoFocus={handleVideoFocus}
onVideoItemMount={(videoRef) => {
this.handleCanvasResize();
Expand All @@ -329,13 +366,16 @@ class VideoList extends Component {
</Styled.VideoListItem>
);
});

return videoItems.concat(userItems);
}

render() {
const {
streams,
intl,
cameraDock,
isGridEnabled,
} = this.props;
const { optimalGrid, autoplayBlocked } = this.state;
const { position } = cameraDock;
Expand All @@ -352,7 +392,7 @@ class VideoList extends Component {
>
{this.renderPreviousPageButton()}

{!streams.length ? null : (
{!streams.length && !isGridEnabled ? null : (
<Styled.VideoList
ref={(ref) => {
this.grid = ref;
Expand Down
@@ -1,24 +1,38 @@
import React from 'react';
import React, { useContext } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import VideoList from '/imports/ui/components/video-provider/video-list/component';
import VideoService from '/imports/ui/components/video-provider/service';
import { layoutSelect, layoutSelectOutput, layoutDispatch } from '../../layout/context';
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';

const VideoListContainer = ({ children, ...props }) => {
const layoutType = layoutSelect((i) => i.layoutType);
const cameraDock = layoutSelectOutput((i) => i.cameraDock);
const layoutContextDispatch = layoutDispatch();
const usingUsersContext = useContext(UsersContext);
const { users: contextUsers } = usingUsersContext;
const { streams, isGridEnabled } = props;

const streamUsers = streams.map((stream) => stream.userId);

const users = isGridEnabled && contextUsers
? Object.values(contextUsers[Auth.meetingID]).filter(
(user) => !user.loggedOut && !user.left && !streamUsers.includes(user.userId)
)
: null;

const { streams } = props;
return (
!streams.length
!streams.length && !isGridEnabled
? null
: (
<VideoList {...{
layoutType,
cameraDock,
layoutContextDispatch,
isGridEnabled,
users,
...props,
}}
>
Expand Down
Expand Up @@ -30,7 +30,7 @@ const VideoListItem = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, onVirtualBgDrop,
makeDragOperations, dragging, draggingOver, isRTL,
makeDragOperations, dragging, draggingOver, isRTL, isStream,
} = props;

const intl = useIntl();
Expand Down Expand Up @@ -129,6 +129,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
/>
);
Expand All @@ -154,6 +155,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
/>
<UserStatus
Expand Down Expand Up @@ -189,6 +191,7 @@ const VideoListItem = (props) => {
cameraId={cameraId}
isFullscreenContext={isFullscreenContext}
layoutContextDispatch={layoutContextDispatch}
isStream={isStream}
/>
</Styled.TopBar>
<Styled.BottomBar>
Expand All @@ -201,6 +204,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
isStream={isStream}
onHandleDisableCam={() => setIsSelfViewDisabled((value) => !value)}
/>
<UserStatus
Expand Down Expand Up @@ -246,7 +250,7 @@ const VideoListItem = (props) => {
{(videoIsReady || isSelfViewDisabled) && (
isVideoSqueezed ? renderSqueezedButton() : renderDefaultButtons()
)}
{!videoIsReady && !isSelfViewDisabled && (
{!videoIsReady && (!isSelfViewDisabled || !isStream) && (
isVideoSqueezed ? renderWebcamConnectingSqueezed() : renderWebcamConnecting()
)}

Expand All @@ -258,6 +262,9 @@ export default withDragAndDrop(injectIntl(VideoListItem));

VideoListItem.defaultProps = {
numOfStreams: 0,
onVideoItemMount: () => {},
onVideoItemUnmount: () => {},
onVirtualBgDrop: () => {},
};

VideoListItem.propTypes = {
Expand All @@ -268,8 +275,9 @@ VideoListItem.propTypes = {
formatMessage: PropTypes.func.isRequired,
}).isRequired,
onHandleVideoFocus: PropTypes.func.isRequired,
onVideoItemMount: PropTypes.func.isRequired,
onVideoItemUnmount: PropTypes.func.isRequired,
onVideoItemMount: PropTypes.func,
onVideoItemUnmount: PropTypes.func,
onVirtualBgDrop: PropTypes.func,
isFullscreenContext: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
user: PropTypes.shape({
Expand Down
Expand Up @@ -24,7 +24,7 @@ const intlMessages = defineMessages({
const ENABLE_WEBCAM_BACKGROUND_UPLOAD = Meteor.settings.public.virtualBackgrounds.enableVirtualBackgroundUpload;

const DragAndDrop = (props) => {
const { children, intl, readFile, onVirtualBgDrop: onAction } = props;
const { children, intl, readFile, onVirtualBgDrop: onAction, isStream } = props;

const [dragging, setDragging] = useState(false);
const [draggingOver, setDraggingOver] = useState(false);
Expand Down Expand Up @@ -106,7 +106,7 @@ const DragAndDrop = (props) => {
};

const makeDragOperations = useCallback((userId) => {
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD) return {};
if (!userId || Auth.userID !== userId || !ENABLE_WEBCAM_BACKGROUND_UPLOAD || !isStream) return {};

const startAndSaveVirtualBackground = (file) => handleStartAndSaveVirtualBackground(file);

Expand Down