From ac4e82d7033876c6fa7150777993028aeb5c7dd8 Mon Sep 17 00:00:00 2001 From: Daria Oldenburg Date: Thu, 28 Nov 2024 22:14:49 +0100 Subject: [PATCH] feat(groupVideoGrid): responsive layout --- src/script/calling/Call.ts | 10 +- .../calling/CallingCell/CallingCell.tsx | 1 + .../calling/FullscreenVideoCall.tsx | 1 + .../components/calling/GroupVideoGrid.tsx | 116 +++++++++++++++++- 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/src/script/calling/Call.ts b/src/script/calling/Call.ts index 460247d8698..b30756d720a 100644 --- a/src/script/calling/Call.ts +++ b/src/script/calling/Call.ts @@ -35,8 +35,6 @@ import type {MediaDevicesHandler} from '../media/MediaDevicesHandler'; export type SerializedConversationId = string; -const NUMBER_OF_PARTICIPANTS_IN_ONE_PAGE = 9; - interface ActiveSpeaker { clientId: string; levelNow: number; @@ -60,6 +58,7 @@ export class Call { public blockMessages: boolean = false; public currentPage: ko.Observable = ko.observable(0); public pages: ko.ObservableArray = ko.observableArray(); + public numberOfParticipantsInOnePage: number = 9; readonly maximizedParticipant: ko.Observable; public readonly isActive: ko.PureComputed; @@ -215,6 +214,11 @@ export class Call { return this.participants().filter(({user, clientId}) => !user.isMe || this.selfClientId !== clientId); } + setNumberOfParticipantsInOnePage(participantsInOnePage: number): void { + this.numberOfParticipantsInOnePage = participantsInOnePage; + this.updatePages(); + } + updatePages() { const selfParticipant = this.getSelfParticipant(); const remoteParticipants = this.getRemoteParticipants().sort((p1, p2) => sortUsersByPriority(p1.user, p2.user)); @@ -223,7 +227,7 @@ export class Call { const newPages = chunk( [selfParticipant, ...withVideo, ...withoutVideo].filter(Boolean), - NUMBER_OF_PARTICIPANTS_IN_ONE_PAGE, + this.numberOfParticipantsInOnePage, ); this.currentPage(Math.min(this.currentPage(), newPages.length - 1)); diff --git a/src/script/components/calling/CallingCell/CallingCell.tsx b/src/script/components/calling/CallingCell/CallingCell.tsx index ea1fd4f4b71..df618fb267c 100644 --- a/src/script/components/calling/CallingCell/CallingCell.tsx +++ b/src/script/components/calling/CallingCell/CallingCell.tsx @@ -310,6 +310,7 @@ export const CallingCell = ({ minimized maximizedParticipant={maximizedParticipant} selfParticipant={selfParticipant} + call={call} setMaximizedParticipant={setMaximizedParticipant} /> diff --git a/src/script/components/calling/FullscreenVideoCall.tsx b/src/script/components/calling/FullscreenVideoCall.tsx index 5587562ba2f..04dcd1cc7d0 100644 --- a/src/script/components/calling/FullscreenVideoCall.tsx +++ b/src/script/components/calling/FullscreenVideoCall.tsx @@ -561,6 +561,7 @@ const FullscreenVideoCall: React.FC = ({ } : videoGrid } + call={call} setMaximizedParticipant={participant => setMaximizedParticipant(call, participant)} /> {classifiedDomains && ( diff --git a/src/script/components/calling/GroupVideoGrid.tsx b/src/script/components/calling/GroupVideoGrid.tsx index c2f2d6056da..67517848925 100644 --- a/src/script/components/calling/GroupVideoGrid.tsx +++ b/src/script/components/calling/GroupVideoGrid.tsx @@ -22,8 +22,12 @@ import React, {CSSProperties, useEffect, useState} from 'react'; import {css} from '@emotion/react'; import {QualifiedId} from '@wireapp/api-client/lib/user'; +import {QUERY} from '@wireapp/react-ui-kit'; + import {Avatar, AVATAR_SIZE} from 'Components/Avatar'; import * as Icon from 'Components/Icon'; +import {useActiveWindowMatchMedia} from 'Hooks/useActiveWindowMatchMedia'; +import {Call} from 'src/script/calling/Call'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; @@ -38,6 +42,7 @@ export interface GroupVideoGripProps { maximizedParticipant: Participant | null; minimized?: boolean; selfParticipant: Participant; + call: Call; setMaximizedParticipant?: (participant: Participant | null) => void; } @@ -46,9 +51,52 @@ interface RowsAndColumns extends CSSProperties { '--rows': number; } -const calculateRowsAndColumns = (totalCount: number): RowsAndColumns => { - const columns = totalCount ? Math.ceil(Math.sqrt(totalCount)) : 1; +const COLUMNS = { + DESKTOP: 3, + DESKTOP_EDGE_CASE: 2, + TABLET: 2, + MOBILE: 1, +}; + +const PARTICIPANTS_DESKTOP_EDGE_CASE = 3; + +const getDesiredColumns = ({ + totalCount, + isDesktop, + isTablet, + isShort, +}: { + totalCount: number; + isDesktop: boolean; + isTablet: boolean; + isShort: boolean; +}): number => { + if (isDesktop) { + // Special case: use different layout for 3 participants when not in short mode + if (totalCount === PARTICIPANTS_DESKTOP_EDGE_CASE && !isShort) { + return COLUMNS.DESKTOP_EDGE_CASE; + } + return COLUMNS.DESKTOP; + } + + if (isTablet) { + return COLUMNS.TABLET; + } + + return COLUMNS.MOBILE; +}; + +const calculateRowsAndColumns = (params: { + totalCount: number; + isTablet: boolean; + isDesktop: boolean; + isShort: boolean; +}): RowsAndColumns => { + const {totalCount} = params; + const desiredColumns = getDesiredColumns(params); + const columns = Math.min(totalCount, desiredColumns); const rows = totalCount ? Math.ceil(totalCount / columns) : 1; + return {'--columns': columns, '--rows': rows}; }; @@ -76,13 +124,27 @@ const GroupVideoThumbnailWrapper: React.FC<{children?: React.ReactNode; minimize ); +const HEIGHT_QUERIES = { + SHORT: 'max-height: 469px', + MEDIUM: 'min-height: 470px) and (max-height: 829px', + TALL: 'min-height: 830px', +}; + const GroupVideoGrid: React.FunctionComponent = ({ minimized = false, grid, selfParticipant, maximizedParticipant, + call, setMaximizedParticipant, }) => { + const isMobile = useActiveWindowMatchMedia(QUERY.mobile); + const isTablet = useActiveWindowMatchMedia(QUERY.tablet); + const isDesktop = useActiveWindowMatchMedia(QUERY.desktop); + const isShort = useActiveWindowMatchMedia(HEIGHT_QUERIES.SHORT); + const isMedium = useActiveWindowMatchMedia(HEIGHT_QUERIES.MEDIUM); + const isTall = useActiveWindowMatchMedia(HEIGHT_QUERIES.TALL); + const thumbnail = useKoSubscribableChildren(grid.thumbnail!, [ 'hasActiveVideo', 'sharesScreen', @@ -90,7 +152,14 @@ const GroupVideoGrid: React.FunctionComponent = ({ 'blurredVideoStream', ]); - const [rowsAndColumns, setRowsAndColumns] = useState(calculateRowsAndColumns(grid?.grid.length)); + const [rowsAndColumns, setRowsAndColumns] = useState( + calculateRowsAndColumns({ + totalCount: grid?.grid.length, + isTablet: isTablet, + isDesktop: isDesktop, + isShort: isShort, + }), + ); const doubleClickedOnVideo = (userId: QualifiedId, clientId: string) => { if (typeof setMaximizedParticipant !== 'function') { @@ -111,8 +180,45 @@ const GroupVideoGrid: React.FunctionComponent = ({ const participants = (maximizedParticipant ? [maximizedParticipant] : grid.grid).filter(Boolean); useEffect(() => { - setRowsAndColumns(calculateRowsAndColumns(participants.length)); - }, [participants.length]); + setRowsAndColumns( + calculateRowsAndColumns({ + totalCount: participants.length, + isTablet: isTablet, + isDesktop: isDesktop, + isShort: isShort, + }), + ); + }, [participants.length, isTablet, isDesktop, isShort]); + + useEffect(() => { + const PARTICIPANTS_LIMITS = { + TABLET: {SHORT: 2, MEDIUM: 4, TALL: 8}, + DESKTOP: {SHORT: 3, MEDIUM: 6, TALL: 9}, + MOBILE: {SHORT: 1, MEDIUM: 2, TALL: 4}, + }; + + const setParticipantsForDevice = (limits: {SHORT: number; MEDIUM: number; TALL: number}) => { + if (isShort) { + return call.setNumberOfParticipantsInOnePage(limits.SHORT); + } + if (isMedium) { + return call.setNumberOfParticipantsInOnePage(limits.MEDIUM); + } + if (isTall) { + return call.setNumberOfParticipantsInOnePage(limits.TALL); + } + }; + + if (isTablet) { + return setParticipantsForDevice(PARTICIPANTS_LIMITS.TABLET); + } + if (isDesktop) { + return setParticipantsForDevice(PARTICIPANTS_LIMITS.DESKTOP); + } + if (isMobile) { + setParticipantsForDevice(PARTICIPANTS_LIMITS.MOBILE); + } + }, [call, isTablet, isDesktop, isMobile, isShort, isMedium, isTall]); const {isMuted: selfIsMuted} = useKoSubscribableChildren(selfParticipant, ['isMuted']);