From a3800e16a5267b2923d77044c8a9d91b8f194390 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 21 Jun 2023 18:33:36 +0200 Subject: [PATCH] Revert "Revert "Group Calls: Redesign (#2859)"" This reverts commit 6c44080fb4d43ab3ce297c5cab7732281eec1411. --- src/assets/fonts/icomoon.woff | Bin 56944 -> 58056 bytes src/assets/fonts/icomoon.woff2 | Bin 26328 -> 26832 bytes .../calls/group/GroupCall.module.scss | 296 ++++++++ src/components/calls/group/GroupCall.scss | 345 ---------- src/components/calls/group/GroupCall.tsx | 633 +++++++++++------- .../group/GroupCallParticipant.module.scss | 55 ++ .../calls/group/GroupCallParticipant.scss | 78 --- .../calls/group/GroupCallParticipant.tsx | 160 +++-- .../GroupCallParticipantList.module.scss | 10 + .../calls/group/GroupCallParticipantList.tsx | 72 +- .../calls/group/GroupCallParticipantMenu.scss | 10 +- .../calls/group/GroupCallParticipantMenu.tsx | 92 ++- .../group/GroupCallParticipantStreams.tsx | 105 --- .../GroupCallParticipantVideo.module.scss | 150 +++++ .../group/GroupCallParticipantVideo.scss | 126 ---- .../calls/group/GroupCallParticipantVideo.tsx | 319 +++++++-- .../calls/group/GroupCallTopPane.scss | 10 +- .../calls/group/MicrophoneButton.module.scss | 52 ++ .../calls/group/MicrophoneButton.scss | 58 -- .../calls/group/MicrophoneButton.tsx | 83 +-- .../calls/group/OutlinedMicrophoneIcon.tsx | 32 +- .../group/hooks/useGroupCallVideoLayout.ts | 237 +++++++ src/components/common/AnimatedSticker.tsx | 20 +- src/components/ui/Button.tsx | 3 + src/components/ui/InfiniteScroll.tsx | 3 + src/components/ui/ListItem.tsx | 4 +- src/global/actions/ui/calls.ts | 8 +- src/global/selectors/calls.ts | 23 +- src/lib/rlottie/RLottie.ts | 8 +- src/lib/secret-sauce/secretsauce.ts | 8 - src/styles/Telegram T.json | 444 ++++++------ src/styles/_variables.scss | 1 + src/styles/icons.scss | 6 + 33 files changed, 2051 insertions(+), 1400 deletions(-) create mode 100644 src/components/calls/group/GroupCall.module.scss delete mode 100644 src/components/calls/group/GroupCall.scss create mode 100644 src/components/calls/group/GroupCallParticipant.module.scss delete mode 100644 src/components/calls/group/GroupCallParticipant.scss create mode 100644 src/components/calls/group/GroupCallParticipantList.module.scss delete mode 100644 src/components/calls/group/GroupCallParticipantStreams.tsx create mode 100644 src/components/calls/group/GroupCallParticipantVideo.module.scss delete mode 100644 src/components/calls/group/GroupCallParticipantVideo.scss create mode 100644 src/components/calls/group/MicrophoneButton.module.scss delete mode 100644 src/components/calls/group/MicrophoneButton.scss create mode 100644 src/components/calls/group/hooks/useGroupCallVideoLayout.ts diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index 607e7229f745683d9227a593868742c6ff5baa2a..023beb669402681b9784418127c947724476032f 100644 GIT binary patch delta 2059 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 !z7w8_nHtl70COXGEUziL;ZT3GqE@ z=1qhyBq>|PK|$0=%q5fY``VAv?sJg-N4r)-QY~5lu!w6ZL064*TmGR8lN6s+=~K2l zSGdC0?sD$>lbrEC=m+H~pJ+|Aen2>EX{nH*6xwN7#B>xj;pC|Mp)K;BK>WOTa8NEB z+nYIy6`8H4ICgB9H!MCArF+*9`OJ8^0{-;w*a2$Rj6{)~ad3n?47S=JkBNlekilaL zJWSBn!HWMf`8l3Adp4ZEJi`~fQ5;BL98Xq9td`4P`hH!r=IXTT8Xav_&fLH{us{_c98hf8!ZvW8XYm&#F5sJWymtA5l`a|VGe>_Ugh+pKx?sC{ zr>}s|2va++Z+PXrBzy8`&FJA4KLOA5)cciEqsE!7oB!>nUf%AM^U#%90^mUzt!%=}h!DiOMplG3F`hWlK`hAzOTtMNx)9zS5C&WXj z9ur7Yfu&ffWyV6>OpSL8cts(q=1bA+6tH7zWLA%%3V#iNJV6N1k_3-{*~6V4-I)>ko(A z(}h+4f7De{@A^DHYJI8@Od**SRnraAvK>yB8(Q%EzX5}WNXf`4D5=CzihcGb&N4Ot}j4El{aSwHmeR)N9bF$wG@Pw!~83SjNOmENUKEt&h3( zx$m1`olwKHY^Uscm1?c-2VoQ^ES;NQSX^3OSrIb=iDW#NBN^7mP)>><=wbt4gV~jDz f7-Nhv##(Ewwboi|t+m!U=bUrSIp>^n?tGd6y$h`% diff --git a/src/components/calls/group/GroupCall.module.scss b/src/components/calls/group/GroupCall.module.scss new file mode 100644 index 000000000..1857dfa94 --- /dev/null +++ b/src/components/calls/group/GroupCall.module.scss @@ -0,0 +1,296 @@ +.root { + --group-call-panel-color: #212121; + --group-call-panel-header-border-color: #3b3b3b; + --group-call-background-color: #000000; + --green-button-color: rgba(1, 200, 80, 0.3); + --blue-button-color: rgb(60, 135, 247, 0.2); + --purple-button-color: rgb(61, 82, 223, 0.2); + --gradient-blue: linear-gradient(225deg, #4EABF8 14.73%, #3478F6 85.27%); + --gradient-green: linear-gradient(230.46deg, #00A3B4 12.94%, #00CB47 86.29%); + --gradient-purple: linear-gradient(230.46deg, #CE4D74 0%, #3D52DF 100%); + --gradient-speaking: linear-gradient(135deg, #5CC85C 0%, #48A1B3 101.27%); + + --red-button-color: rgba(255, 89, 90, 0.3); + --disabled-button-color: #333333; + --color-text-secondary: #AAAAAA; + --color-text: #FFFFFF; + + --default-width: 26.25rem; + --max-height: 40rem; + + color: var(--color-text); + + :global { + .modal-dialog { + max-width: var(--default-width); + max-height: min(var(--max-height), 100vh); + height: 100%; + min-height: min(80vh, var(--max-height)); + overflow: hidden; + background: var(--group-call-background-color); + } + + .modal-content { + min-height: 100%; + + display: flex; + + padding: 0; + } + } +} + +.panelWrapper { + max-width: var(--default-width); + width: 100%; +} + +.panel { + background: var(--group-call-panel-color); + + display: flex; + flex-direction: column; + height: 100%; + + overflow: auto; + position: relative; +} + +.panelScrollTrigger { + position: absolute; + top: 0; + width: 100%; +} + +.panelHeader { + display: flex; + align-items: center; + position: sticky; + top: 0; + + border-bottom: 0.0625rem solid transparent; + + padding: 0.375rem 0.875rem; + user-select: none; + z-index: 1; + background: var(--group-call-panel-color); + + transition: 0.25s ease-in-out border-bottom-color; + + &.scrolled { + border-bottom-color: var(--group-call-panel-header-border-color); + } +} + +.headerButton { + color: var(--color-text) !important; +} + +.firstButton { + margin-right: 1.375rem; +} + +.lastButton { + margin-left: auto; +} + +.panelHeaderText { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; +} + +.title { + line-height: 1.375rem; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + unicode-bidi: plaintext; + font-size: 1rem; + font-weight: 500; + margin: 0; +} + +.bigger { + font-size: 1.25rem; +} + +.subtitle { + font-size: 0.875rem; + line-height: 1.125rem; + margin: 0; + color: var(--color-text-secondary); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; +} + +.participants { + position: relative; + margin: 0.125rem 0.5rem 0; +} + +.participantVideos { + width: 100%; + position: relative; +} + +.addParticipantButton { + position: fixed; +} + +.videos { + display: flex; + flex-direction: column; + + width: calc(100% - var(--default-width)); +} + +.videosHeader { + display: flex; + align-items: center; + padding: 0.375rem 0.875rem; +} + +.videosHeaderLastButton { + margin-left: auto; +} + +.videosContent { + flex-grow: 1; + margin: 0.5rem 0.625rem; +} + +.actions { + --actions-max-width: 0px; + position: absolute; + left: 50%; + transform: translateX(calc(-50% - var(--actions-max-width) / 2)); + bottom: 1.75rem; + + display: flex; + gap: 1.25rem; + z-index: 2; +} + + +.actionButton { + width: 3.375rem !important; + height: 3.375rem !important; + color: var(--color-text) !important; + background-color: var(--green-button-color) !important; + transition: 0.15s filter, 0.25s ease-out background-color; + backdrop-filter: blur(25px); + + &:global(.disabled) { + background: var(--disabled-button-color) !important; + } + + &:hover { + filter: brightness(1.1); + } +} + +.destructive { + background: var(--red-button-color) !important; +} + +.canRequestToSpeak { + background: var(--purple-button-color) !important; +} + +.muted { + background: var(--blue-button-color) !important; +} + +.fullscreen { + :global { + .modal-dialog { + min-width: 100%; + min-height: 100%; + border-radius: 0; + } + + .modal-content { + max-height: initial; + } + } + + &.portrait .panelWrapper { + max-width: 100%; + } +} + +.landscape { + .panelWrapper { + position: absolute; + right: 0; + top: 0; + bottom: 0; + + transform: translateZ(0); + } + + &:not(.noVideoParticipants) { + .actions { + --actions-max-width: var(--default-width); + + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(50px); + border-radius: 1.25rem; + padding: 0.75rem; + bottom: 2.5rem; + transition: var(--layer-transition) transform, 250ms ease-in-out opacity; + opacity: 0; + } + + .videos:hover ~ .actions, .video:hover ~ .actions, .actions:hover { + opacity: 1; + } + } + + &.noVideoParticipants { + .panelWrapper { + max-width: max(50vw, 30rem); + width: 100%; + transform: translateX(-50%); + left: 50%; + right: 0; + } + + :global(.modal-content) { + background: var(--group-call-panel-color); + } + } +} + +.portrait { + .panelWrapper::after { + display: block; + content: ''; + position: fixed; + width: 100%; + height: 7.5rem; + left: 0; + bottom: 0; + pointer-events: none; + + background: linear-gradient(180deg, rgba(33, 33, 33, 0) 0%, rgba(33, 33, 33, 0.65) 48.54%, #212121 100%); + } +} + +.noSidebar { + .panelWrapper { + transform: translate3d(100%, 0, 0); + } + + .videos { + width: 100%; + } + + .actions { + --actions-max-width: 0px !important; + } +} diff --git a/src/components/calls/group/GroupCall.scss b/src/components/calls/group/GroupCall.scss deleted file mode 100644 index f2735ffd3..000000000 --- a/src/components/calls/group/GroupCall.scss +++ /dev/null @@ -1,345 +0,0 @@ -.GroupCall { - .modal-content { - display: flex; - flex-direction: column; - align-items: center; - height: 37.5rem; - } - - .modal-dialog { - max-height: calc(100% - 4rem); - background: #181f27; - } - - .Menu { - --color-text: white; - --color-background-compact-menu: #212121DD; - --color-background-compact-menu-hover: #00000066; - .bubble { - box-shadow: 0 0.25rem 0.5rem 0.125rem rgba(16, 16, 16, 0.3); - } - } - - &.single-column { - opacity: 1 !important; - - .modal-dialog { - max-width: 100% !important; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - margin-top: auto; - margin-bottom: 0; - transform: translate3d(0, 100%, 0); - transition: transform 0.3s ease, opacity 0.3s ease; - } - - .modal-backdrop { - opacity: 0; - transition: opacity 0.2s ease; - } - - &.open { - .modal-backdrop { - opacity: 1; - } - - .modal-dialog { - transform: translate3d(0, 0, 0); - } - } - } - - .header { - width: 100%; - display: flex; - align-items: center; - color: #fff; - margin-bottom: 0.5rem; - - h3 { - font-size: 1.25rem; - font-weight: 500; - margin: 0 auto 0 0.5rem; - } - } - - .videos { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - } - - .participants { - margin-top: 0.75rem; - background: #222b34; - border-radius: 0.75rem; - - .Loading { - padding: 2rem 0; - } - - .invite-btn { - padding: 0.25rem 0.75rem; - display: flex; - align-items: center; - border-radius: 0.75rem; - transition: 0.15s ease-out background-color; - cursor: var(--custom-cursor, pointer); - color: var(--color-text-secondary); - - &:hover { - background: #2f363e; - } - - .text { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .icon-wrapper { - display: flex; - justify-content: center; - align-items: center; - width: 2.75rem; - height: 2.75rem; - font-size: 1.5rem; - margin-right: 1rem; - } - } - } - - .scrollable { - overflow: auto; - padding-bottom: 2rem; - max-width: 37.5rem; - width: 100%; - } - - .buttons { - max-width: 37.5rem; - margin-top: auto; - display: flex; - align-items: center; - justify-content: space-around; - width: 100%; - position: relative; - height: 8.75rem; - - button { - cursor: var(--custom-cursor, pointer); - } - - &::before { - position: absolute; - content: ""; - width: 100%; - height: 2rem; - background: linear-gradient(0deg, #181f27, rgba(24, 31, 39, 0)); - z-index: 0; - top: -2rem; - } - - .button-wrapper { - width: 4rem; - display: flex; - flex-direction: column; - align-items: center; - - .button-text { - white-space: nowrap; - font-size: 0.75rem; - margin-top: 0.5rem; - color: #fff; - } - - &.microphone-wrapper { - width: 6rem; - - .button-text { - margin-top: 0.75rem; - font-size: 1rem; - } - } - } - - .Loading { - position: absolute; - transform: translate(0, -1.125rem); - - .Spinner { - --spinner-size: 6.5rem; - } - } - - .video-buttons { - display: flex; - flex-direction: column; - align-items: center; - } - - .small-button, - .smaller-button { - outline: none; - border: 0; - background: #15415b; - border-radius: 50%; - width: 3rem; - height: 3rem; - color: #fff; - font-size: 1.375rem; - display: flex; - align-items: center; - justify-content: center; - transition: 0.25s ease-out background-color; - - &:hover { - background: #11364b; - } - } - - .small-button.camera.active { - background: #15415b; - - &:hover { - background: #11364b; - } - } - - .small-button.speaker { - background: #2b3a51; - - &.active { - background: #496092; - } - } - - .small-button.leave { - background: #5a2824; - - &:hover { - background: #49201d; - } - } - - .smaller-button { - width: 2.5rem; - height: 2.5rem; - margin-bottom: 0.5rem; - padding: 0; - } - } - - &.landscape .scrollable { - display: flex; - flex-direction: row; - flex-grow: 1; - gap: 1rem; - align-items: flex-start; - max-width: 100%; - max-height: 100%; - } - - &.landscape .GroupCallParticipantVideo { - max-height: initial; - - video { - height: 100%; - } - } - - &.landscape .buttons { - position: absolute; - left: calc(50% - 15.625rem / 2); - transform: translateX(-50%); - width: auto; - gap: 1rem; - bottom: 4rem; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(10px); - border-radius: 1rem; - z-index: 5; - padding: 0.75rem 1rem; - height: auto; - - .button-text { - display: none; - } - - .video-buttons { - flex-direction: row; - gap: 1rem; - - .smaller-button { - margin-bottom: 0; - } - } - - .Loading { - transform: none; - .Spinner { - --spinner-size: 3.25rem; - } - } - - .MicrophoneButton { - canvas { - width: 2rem !important; - height: 2rem !important; - } - } - - .MicrophoneButton, - .microphone-wrapper { - width: 3rem; - height: 3rem; - - .AnimatedSticker { - display: flex; - align-items: center; - justify-content: center; - } - } - - &::before { - display: none; - } - } - - &.landscape.no-sidebar .buttons { - left: calc(50%); - } - - &.landscape .streams { - width: 100%; - height: 100%; - } - - &.landscape .videos { - width: 100%; - height: 100%; - - display: grid; - --column-count: 1; - grid-template-columns: repeat(var(--column-count), 1fr); - grid-auto-rows: 1fr; - - .GroupCallParticipantVideo { - max-height: 100%; - width: 100%; - - .thumbnail-wrapper { - height: 100%; - } - } - - &.span-last-video .GroupCallParticipantVideo:last-child { - grid-column: span var(--column-count); - } - } - - &.landscape .participants { - width: 15.625rem; - margin-top: 0; - } -} diff --git a/src/components/calls/group/GroupCall.tsx b/src/components/calls/group/GroupCall.tsx index 71abc5d43..a05e210fa 100644 --- a/src/components/calls/group/GroupCall.tsx +++ b/src/components/calls/group/GroupCall.tsx @@ -1,51 +1,43 @@ -import type { - GroupCallConnectionState, GroupCallParticipant as TypeGroupCallParticipant, -} from '../../../lib/secret-sauce'; -import { - IS_SCREENSHARE_SUPPORTED, switchCameraInput, toggleSpeaker, -} from '../../../lib/secret-sauce'; -import type { FC } from '../../../lib/teact/teact'; import React, { - memo, useCallback, useEffect, useMemo, useRef, useState, + memo, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import '../../../global/actions/calls'; -import type { IAnchorPosition } from '../../../types'; +import type { + GroupCallConnectionState, GroupCallParticipant as TypeGroupCallParticipant, +} from '../../../lib/secret-sauce'; +import type { FC } from '../../../lib/teact/teact'; +import type { VideoParticipant } from './hooks/useGroupCallVideoLayout'; -import { - IS_ANDROID, - IS_IOS, - IS_REQUEST_FULLSCREEN_SUPPORTED, -} from '../../../util/windowEnvironment'; -import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; +import { IS_SCREENSHARE_SUPPORTED } from '../../../lib/secret-sauce'; import buildClassName from '../../../util/buildClassName'; import { + selectCanInviteToActiveGroupCall, selectGroupCall, selectGroupCallParticipant, selectIsAdminInActiveGroupCall, } from '../../../global/selectors/calls'; -import { selectTabState } from '../../../global/selectors'; +import { selectChat, selectTabState } from '../../../global/selectors'; +import { compact } from '../../../util/iteratees'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; import useAppLayout from '../../../hooks/useAppLayout'; +import useGroupCallVideoLayout from './hooks/useGroupCallVideoLayout'; +import { useIntersectionObserver, useIsIntersecting } from '../../../hooks/useIntersectionObserver'; +import useLastCallback from '../../../hooks/useLastCallback'; -import Loading from '../../ui/Loading'; import Button from '../../ui/Button'; -import DropdownMenu from '../../ui/DropdownMenu'; -import MenuItem from '../../ui/MenuItem'; import Modal from '../../ui/Modal'; import MicrophoneButton from './MicrophoneButton'; -import AnimatedIcon from '../../common/AnimatedIcon'; import Checkbox from '../../ui/Checkbox'; -import GroupCallParticipantMenu from './GroupCallParticipantMenu'; import GroupCallParticipantList from './GroupCallParticipantList'; -import GroupCallParticipantStreams from './GroupCallParticipantStreams'; +import FloatingActionButton from '../../ui/FloatingActionButton'; +import GroupCallParticipantVideo from './GroupCallParticipantVideo'; -import './GroupCall.scss'; +import styles from './GroupCall.module.scss'; -const CAMERA_FLIP_PLAY_SEGMENT: [number, number] = [0, 10]; -const PARTICIPANT_HEIGHT = 60; +const INTERSECTION_THROTTLE = 200; export type OwnProps = { groupCallId: string; @@ -57,21 +49,21 @@ type StateProps = { title?: string; meParticipant?: TypeGroupCallParticipant; participantsCount?: number; - isSpeakerEnabled?: boolean; isAdmin: boolean; participants: Record; + canInvite: boolean; }; const GroupCall: FC = ({ groupCallId, isCallPanelVisible, connectionState, - isSpeakerEnabled, + participantsCount, title, meParticipant, isAdmin, participants, - + canInvite, }) => { const { toggleGroupCallVideo, @@ -80,24 +72,60 @@ const GroupCall: FC = ({ toggleGroupCallPanel, connectToActiveGroupCall, playGroupCallSound, + createGroupCallInviteLink, } = getActions(); const lang = useLang(); // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); - const { isMobile, isLandscape } = useAppLayout(); + + // eslint-disable-next-line no-null/no-null + const primaryVideoContainerRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const secondaryVideoContainerRef = useRef(null); + + // eslint-disable-next-line no-null/no-null + const panelScrollTriggerRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const panelRef = useRef(null); const [isLeaving, setIsLeaving] = useState(false); + const isOpen = !isCallPanelVisible && !isLeaving; + + const { observe } = useIntersectionObserver({ + rootRef: panelRef, + throttleMs: INTERSECTION_THROTTLE, + isDisabled: !isOpen, + }); + + const hasScrolled = !useIsIntersecting(panelScrollTriggerRef, isOpen ? observe : undefined); + + const { isMobile, isLandscape } = useAppLayout(); + const [isFullscreen, openFullscreen, closeFullscreen] = useFlag(); const [isSidebarOpen, openSidebar, closeSidebar] = useFlag(true); - const hasVideoParticipants = Object.values(participants).some(({ video, presentation }) => video || presentation); - const isLandscapeLayout = isFullscreen && (!isMobile || isLandscape) && hasVideoParticipants; + const isLandscapeLayout = Boolean(isFullscreen && isLandscape); - const [participantMenu, setParticipantMenu] = useState<{ - participant: TypeGroupCallParticipant; - anchor: IAnchorPosition; - } | undefined>(); - const [isParticipantMenuOpen, openParticipantMenu, closeParticipantMenu] = useFlag(); + const firstPresentation = useMemo(() => { + return Object.values(participants).find(({ presentation }) => presentation); + }, [participants]); + const videoParticipants = useMemo(() => Object.values(participants) + .filter(({ video, presentation }) => video || presentation) + .flatMap(({ id, video, presentation }) => compact([ + video ? { + id, + type: 'video' as const, + } : undefined, + presentation ? { + id, + type: 'screen' as const, + } : undefined, + ])), + [participants]); + const hasVideoParticipants = videoParticipants.length > 0; + + const groupCallTitle = title || lang('VoipGroupVoiceChat'); + const membersString = lang('Participants', participantsCount, 'i'); const [isConfirmLeaveModalOpen, openConfirmLeaveModal, closeConfirmLeaveModal] = useFlag(); const [isEndGroupCallModal, setIsEndGroupCallModal] = useState(false); @@ -105,20 +133,10 @@ const GroupCall: FC = ({ const hasVideo = meParticipant?.hasVideoStream; const hasPresentation = meParticipant?.hasPresentationStream; + const hasAudioStream = meParticipant?.hasAudioStream; const isConnecting = connectionState !== 'connected'; const canSelfUnmute = meParticipant?.canSelfUnmute; - const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted; - const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => { - const rect = anchor.getBoundingClientRect(); - const container = containerRef.current!; - - setParticipantMenu({ - anchor: { x: rect.left, y: rect.top - container.offsetTop + PARTICIPANT_HEIGHT }, - participant, - }); - - openParticipantMenu(); - }, [openParticipantMenu]); + const canRequestToSpeak = !canSelfUnmute && !hasAudioStream; useEffect(() => { if (connectionState === 'connected') { @@ -126,263 +144,360 @@ const GroupCall: FC = ({ } else if (connectionState === 'reconnecting') { playGroupCallSound({ sound: 'connecting' }); } - }, [connectionState, playGroupCallSound]); + }, [connectionState]); - const handleCloseConfirmLeaveModal = useCallback(() => { + const handleCloseConfirmLeaveModal = useLastCallback(() => { closeConfirmLeaveModal(); setIsEndGroupCallModal(false); - }, [closeConfirmLeaveModal]); + }); - const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { - return ({ onTrigger, isOpen }) => ( - - ); - }, [lang]); - - const handleToggleFullscreen = useCallback(() => { + const handleToggleFullscreen = useLastCallback(() => { if (!containerRef.current) return; - if (isFullscreen) { - document.exitFullscreen().then(closeFullscreen); - } else { - containerRef.current.requestFullscreen().then(openFullscreen); - } - }, [closeFullscreen, isFullscreen, openFullscreen]); - - const handleToggleSidebar = useCallback(() => { - if (isSidebarOpen) { - closeSidebar(); - } else { - openSidebar(); - } - }, [closeSidebar, isSidebarOpen, openSidebar]); - - const handleStreamsDoubleClick = useCallback(() => { - if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return; - - if (!isFullscreen) { - closeSidebar(); - handleToggleFullscreen(); - } else { - handleToggleFullscreen(); - } - }, [closeSidebar, handleToggleFullscreen, isFullscreen]); - - const toggleFullscreen = useCallback(() => { if (isFullscreen) { closeFullscreen(); } else { openFullscreen(); } - }, [closeFullscreen, isFullscreen, openFullscreen]); + }); - const handleClose = useCallback(() => { - toggleGroupCallPanel(); - if (isFullscreen) { - closeFullscreen(); - } - }, [closeFullscreen, isFullscreen, toggleGroupCallPanel]); - - useEffect(() => { - if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return undefined; - const container = containerRef.current; - if (!container) return undefined; - - container.addEventListener('fullscreenchange', toggleFullscreen); - - return () => { - container.removeEventListener('fullscreenchange', toggleFullscreen); - }; - }, [toggleFullscreen]); - - const handleClickVideoOrSpeaker = () => { - if (shouldRaiseHand) { - toggleSpeaker(); + const handleToggleSidebar = useLastCallback(() => { + if (isSidebarOpen) { + closeSidebar(); } else { - toggleGroupCallVideo(); + openSidebar(); } - }; + }); + + const handleInviteMember = useLastCallback(() => { + createGroupCallInviteLink(); + }); + + const handleClickVideo = useLastCallback(() => { + toggleGroupCallVideo(); + }); useEffect(() => { connectToActiveGroupCall(); }, [connectToActiveGroupCall, groupCallId]); - const endGroupCall = useCallback(() => { - setIsEndGroupCallModal(true); - setShouldEndGroupCall(true); - openConfirmLeaveModal(); - if (isFullscreen) { - handleToggleFullscreen(); - } - }, [handleToggleFullscreen, isFullscreen, openConfirmLeaveModal]); - - const handleLeaveGroupCall = useCallback(() => { + const handleLeaveGroupCall = useLastCallback(() => { if (isAdmin && !isConfirmLeaveModalOpen) { openConfirmLeaveModal(); - if (isFullscreen) { - handleToggleFullscreen(); - } return; } playGroupCallSound({ sound: 'leave' }); setIsLeaving(true); closeConfirmLeaveModal(); - }, [ - closeConfirmLeaveModal, handleToggleFullscreen, isAdmin, isConfirmLeaveModalOpen, isFullscreen, - openConfirmLeaveModal, playGroupCallSound, - ]); + }); - const handleCloseAnimationEnd = useCallback(() => { - if (isLeaving) { - leaveGroupCall({ - shouldDiscard: shouldEndGroupCall, - }); - } - }, [isLeaving, leaveGroupCall, shouldEndGroupCall]); + const handleCloseAnimationEnd = useLastCallback(() => { + if (!isLeaving) return; - const handleToggleGroupCallPresentation = useCallback(() => { + leaveGroupCall({ + shouldDiscard: shouldEndGroupCall, + }); + }); + + const handleToggleGroupCallPresentation = useLastCallback(() => { toggleGroupCallPresentation(); - }, [toggleGroupCallPresentation]); + }); + + const canPinVideo = videoParticipants.length > 1 && isLandscapeLayout; + const isLandscapeWithVideos = isLandscapeLayout && hasVideoParticipants; + const [pinnedVideo, setPinnedVideo] = useState(undefined); + const { + videoLayout, + panelOffset, + } = useGroupCallVideoLayout({ + primaryContainerRef: primaryVideoContainerRef, + secondaryContainerRef: secondaryVideoContainerRef, + videoParticipants, + isLandscapeLayout, + pinnedVideo, + }); + + const handleOpenFirstPresentation = useLastCallback(() => { + if (!firstPresentation) return; + + setPinnedVideo({ + id: firstPresentation.id, + type: 'screen', + }); + }); + + useEffect(handleOpenFirstPresentation, [handleOpenFirstPresentation, Boolean(firstPresentation)]); + + useEffect(() => { + if (!pinnedVideo) return; + if (!videoParticipants.some((l) => l.type === pinnedVideo.type && l.id === pinnedVideo.id)) { + setPinnedVideo(undefined); + } + }, [pinnedVideo, videoLayout, videoParticipants]); return ( -
-

{title || lang('VoipGroupVoiceChat')}

- {IS_REQUEST_FULLSCREEN_SUPPORTED && ( - - )} - {isLandscapeLayout && ( - - )} - {((IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand) || isAdmin) && ( - - {IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && ( - +
+ + +

+ {title || lang('VoipGroupVoiceChat')} +

+ + {isLandscapeWithVideos && !isSidebarOpen && ( + )} - {isAdmin && ( - + +
+
+ )} + +
+
+
+ +
+ {!isLandscapeWithVideos && ( + )} - - )} + + {isLandscapeWithVideos && ( + + )} + +
+

+ {isLandscapeWithVideos ? membersString : groupCallTitle} +

+ {!isLandscapeWithVideos && ( + + {membersString} + + )} +
+ + {!isLandscapeWithVideos && canInvite && ( + + )} +
+ +
+
+ {videoLayout.map((layout) => { + const participant = participants[layout.participantId]; + if (!layout.isRemounted || !participant) { + return ( +
+ ); + } + + return ( + + ); + })} +
+ +
+
+ + + + +
+ + {videoLayout.map((layout) => { + const participant = participants[layout.participantId]; + if (layout.isRemounted || !participant) { + return ( +
+ ); + } + return ( + + ); + })} + +
-
-
- + - {(!isLandscapeLayout || isSidebarOpen) - && } -
+ - + -
- {isConnecting && } - -
-
- {hasVideo && (IS_ANDROID || IS_IOS) && ( - - )} - -
- -
- {lang(shouldRaiseHand ? 'VoipSpeaker' : 'VoipCamera')} -
-
- - - -
- - -
- {lang('VoipGroupLeave')} -
-
+
= ({ export default memo(withGlobal( (global, { groupCallId }): StateProps => { const { - connectionState, title, isSpeakerDisabled, participants, participantsCount, + connectionState, title, participants, participantsCount, chatId, } = selectGroupCall(global, groupCallId)! || {}; + const chat = chatId ? selectChat(global, chatId) : undefined; + return { connectionState, - title, - isSpeakerEnabled: !isSpeakerDisabled, + title: title || chat?.title, participantsCount, meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!), isCallPanelVisible: Boolean(selectTabState(global).isCallPanelVisible), isAdmin: selectIsAdminInActiveGroupCall(global), participants, + canInvite: selectCanInviteToActiveGroupCall(global), }; }, )(GroupCall)); diff --git a/src/components/calls/group/GroupCallParticipant.module.scss b/src/components/calls/group/GroupCallParticipant.module.scss new file mode 100644 index 000000000..2945ba2ff --- /dev/null +++ b/src/components/calls/group/GroupCallParticipant.module.scss @@ -0,0 +1,55 @@ +.root { + :global { + .ListItem-button { + --color-chat-hover: rgba(255, 255, 255, 0.04); + padding: 0.5rem; + } + + .multiline-item { + align-self: center; + } + + .title { + display: flex !important; + + .fullName { + font-weight: 500; + font-size: 1rem; + } + } + } +} + +.subtitle { + display: flex !important; + align-items: center; + gap: 0.375rem; + font-size: 1rem !important; +} + +.subtitleText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.icon { + margin-left: auto; + margin-right: 0.25rem; +} + +.subtitleBlue { + --color-text-secondary: #4da6e0; +} + +.subtitleRed { + --color-text-secondary: #ff706f; +} + +.subtitleGreen { + --color-text-secondary: #57bc6c; +} + +.avatar { + margin-right: 0.5rem; +} diff --git a/src/components/calls/group/GroupCallParticipant.scss b/src/components/calls/group/GroupCallParticipant.scss deleted file mode 100644 index 318aa0df5..000000000 --- a/src/components/calls/group/GroupCallParticipant.scss +++ /dev/null @@ -1,78 +0,0 @@ -.GroupCallParticipant { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - color: #fff; - padding: 0.5rem 0.75rem; - border-radius: 0.75rem; - transition: 0.15s ease-out background-color; - cursor: var(--custom-cursor, pointer); - - &:hover { - background: #2f363e; - } - - audio { - display: none; - } - - .Avatar { - margin-right: 1rem; - } - - .info { - min-width: 0; - display: flex; - flex-direction: column; - - .name { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .about { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: #848d94; - font-size: 0.75rem; - - &.blue { - color: #4da6e0; - } - - &.green { - color: #57bc6c; - } - - &.red { - color: #ff706f; - } - } - } - - .microphone { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 2.75rem; - height: 2.75rem; - margin-left: auto; - font-size: 1.5rem; - color: #ff706f; - } - - &.can-self-unmute { - .microphone { - color: #848d94; - } - } - - .streams { - cursor: var(--custom-cursor, pointer); - display: flex; - } -} diff --git a/src/components/calls/group/GroupCallParticipant.tsx b/src/components/calls/group/GroupCallParticipant.tsx index d1b10c89c..da36db660 100644 --- a/src/components/calls/group/GroupCallParticipant.tsx +++ b/src/components/calls/group/GroupCallParticipant.tsx @@ -1,24 +1,31 @@ import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; import { THRESHOLD } from '../../../lib/secret-sauce'; import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useMemo, useRef } from '../../../lib/teact/teact'; +import React, { + memo, useCallback, useMemo, useRef, +} from '../../../lib/teact/teact'; import { withGlobal } from '../../../global'; import type { ApiChat, ApiUser } from '../../../api/types'; +import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; import buildClassName from '../../../util/buildClassName'; +import renderText from '../../common/helpers/renderText'; import { selectChat, selectUser } from '../../../global/selectors'; import useLang from '../../../hooks/useLang'; -import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; +import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; +import useMenuPosition from '../../../hooks/useMenuPosition'; import Avatar from '../../common/Avatar'; import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon'; +import ListItem from '../../ui/ListItem'; +import GroupCallParticipantMenu from './GroupCallParticipantMenu'; +import FullNameTitle from '../../common/FullNameTitle'; -import './GroupCallParticipant.scss'; +import styles from './GroupCallParticipant.module.scss'; type OwnProps = { participant: TypeGroupCallParticipant; - openParticipantMenu: (anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => void; }; type StateProps = { @@ -27,67 +34,132 @@ type StateProps = { }; const GroupCallParticipant: FC = ({ - openParticipantMenu, participant, user, chat, }) => { // eslint-disable-next-line no-null/no-null - const anchorRef = useRef(null); + const ref = useRef(null); + // eslint-disable-next-line no-null/no-null + const menuRef = useRef(null); const lang = useLang(); - const { isSelf, isMutedByMe, isMuted } = participant; + const { + isSelf, isMutedByMe, isMuted, hasVideoStream, hasPresentationStream, + } = participant; const isSpeaking = (participant.amplitude || 0) > THRESHOLD; const isRaiseHand = Boolean(participant.raiseHandRating); - const handleOnClick = () => { - if (isSelf) return; - openParticipantMenu(anchorRef.current!, participant); - }; + const { + isContextMenuOpen, + contextMenuPosition, + handleContextMenu, + handleBeforeContextMenu, + handleContextMenuClose, + handleContextMenuHide, + } = useContextMenuHandlers(ref, isSelf); + + const getTriggerElement = useCallback(() => ref.current, []); + + const getRootElement = useCallback( + () => ref.current!.closest('.custom-scroll, .no-scrollbar'), + [], + ); + + const getMenuElement = useCallback( + () => menuRef.current!, + [], + ); + + const getLayout = useCallback( + () => ({ withPortal: true }), + [], + ); + + const { + positionX, positionY, transformOriginX, transformOriginY, style: menuStyle, + } = useMenuPosition( + contextMenuPosition, + getTriggerElement, + getRootElement, + getMenuElement, + getLayout, + ); + + const hasCustomVolume = Boolean( + !isMuted && isSpeaking && participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME, + ); const [aboutText, aboutColor] = useMemo(() => { - if (isSelf) { - return [lang('ThisIsYou'), 'blue']; - } if (isMutedByMe) { - return [lang('VoipGroupMutedForMe'), 'red']; + return [lang('VoipGroupMutedForMe'), styles.subtitleRed]; } - return isRaiseHand - ? [lang('WantsToSpeak'), 'blue'] - : (!isMuted && isSpeaking ? [ - participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME - ? lang('SpeakingWithVolume', - (participant.volume / GROUP_CALL_VOLUME_MULTIPLIER).toString()) - .replace('%%', '%') : lang('Speaking'), - 'green', - ] - : (participant.about ? [participant.about, ''] : [lang('Listening'), 'blue'])); - }, [isSpeaking, participant.volume, lang, isSelf, isMutedByMe, isRaiseHand, isMuted, participant.about]); + + if (isRaiseHand) { + return [lang('WantsToSpeak'), styles.subtitleBlue]; + } + + if (hasCustomVolume) { + return [ + lang('SpeakingWithVolume', + (participant.volume! / GROUP_CALL_VOLUME_MULTIPLIER).toString()) + .replace('%%', '%'), + styles.subtitleGreen, + ]; + } + + if (!isMuted && isSpeaking) { + return [ + lang('Speaking'), + styles.subtitleGreen, + ]; + } + + if (isSelf) { + return [lang('ThisIsYou'), styles.subtitleBlue]; + } + + return participant.about ? [participant.about, ''] : [lang('Listening'), styles.subtitleBlue]; + }, [ + isMutedByMe, isRaiseHand, isSelf, hasCustomVolume, isMuted, isSpeaking, participant.about, participant.volume, lang, + ]); if (!user && !chat) { return undefined; } - const name = user ? `${user.firstName || ''} ${user.lastName || ''}` : chat?.title; - return ( -
} + rightElement={} + className={styles.root} + onClick={handleContextMenu} + onMouseDown={handleBeforeContextMenu} + onContextMenu={handleContextMenu} + multiline + ripple + ref={ref} > - -
- {name} - {aboutText} -
-
- -
-
+ + + {hasPresentationStream && } + {hasVideoStream && } + {hasCustomVolume && } + {renderText(aboutText)} + + + ); }; diff --git a/src/components/calls/group/GroupCallParticipantList.module.scss b/src/components/calls/group/GroupCallParticipantList.module.scss new file mode 100644 index 000000000..6c9b10ede --- /dev/null +++ b/src/components/calls/group/GroupCallParticipantList.module.scss @@ -0,0 +1,10 @@ +.root { + position: absolute; + width: 100%; + top: 0.25rem; + padding-bottom: 5rem; +} + +.portrait { + padding-bottom: 6rem; +} diff --git a/src/components/calls/group/GroupCallParticipantList.tsx b/src/components/calls/group/GroupCallParticipantList.tsx index 21681ae6d..fe2cc1b87 100644 --- a/src/components/calls/group/GroupCallParticipantList.tsx +++ b/src/components/calls/group/GroupCallParticipantList.tsx @@ -1,17 +1,22 @@ -import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; +import React, { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import useLang from '../../../hooks/useLang'; +import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; +import type { FC } from '../../../lib/teact/teact'; + +import buildClassName from '../../../util/buildClassName'; import { selectActiveGroupCall } from '../../../global/selectors/calls'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; +import useLastCallback from '../../../hooks/useLastCallback'; import GroupCallParticipant from './GroupCallParticipant'; import InfiniteScroll from '../../ui/InfiniteScroll'; +import styles from './GroupCallParticipantList.module.scss'; + type OwnProps = { - openParticipantMenu: (anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => void; + panelOffset: number; + isLandscape: boolean; }; type StateProps = { @@ -21,24 +26,22 @@ type StateProps = { }; const GroupCallParticipantList: FC = ({ + panelOffset, participants, participantsCount, - openParticipantMenu, + isLandscape, }) => { const { - createGroupCallInviteLink, loadMoreGroupCallParticipants, } = getActions(); - const lang = useLang(); - const participantsIds = useMemo(() => { return Object.keys(participants || {}); }, [participants]); - const handleLoadMoreGroupCallParticipants = useCallback(() => { + const handleLoadMoreGroupCallParticipants = useLastCallback(() => { loadMoreGroupCallParticipants(); - }, [loadMoreGroupCallParticipants]); + }); const [viewportIds, getMore] = useInfiniteScroll( handleLoadMoreGroupCallParticipants, @@ -46,37 +49,24 @@ const GroupCallParticipantList: FC = ({ participantsIds.length >= participantsCount, ); - function handleCreateGroupCallInviteLink() { - createGroupCallInviteLink(); - } - return ( -
-
-
- -
-
{lang('VoipGroupInviteMember')}
-
- - - {viewportIds?.map( - (participantId) => ( - participants![participantId] && ( - - ) - ), - )} - - -
+ + {participants && viewportIds?.map( + (participantId) => ( + participants[participantId] && ( + + ) + ), + )} + ); }; diff --git a/src/components/calls/group/GroupCallParticipantMenu.scss b/src/components/calls/group/GroupCallParticipantMenu.scss index e43265a9c..6a2c61630 100644 --- a/src/components/calls/group/GroupCallParticipantMenu.scss +++ b/src/components/calls/group/GroupCallParticipantMenu.scss @@ -1,12 +1,15 @@ @import '../../../styles/mixins'; .participant-menu { - position: absolute; --color-text: white; --color-background-compact-menu: #212121DD; --color-background-compact-menu-hover: #00000066; + position: absolute; + z-index: var(--z-modal-menu); + .bubble { + backdrop-filter: none !important; background: none !important; border-radius: 0; padding: 0; @@ -21,6 +24,7 @@ background: var(--color-background); border-radius: var(--border-radius-default); margin-bottom: 0.5rem; + backdrop-filter: blur(10px); } } @@ -44,7 +48,7 @@ padding: 0.75rem 1rem; .AnimatedSticker { - margin-right: 2rem; + margin-right: 1rem; } } @@ -86,7 +90,7 @@ position: absolute; left: -1.5rem; top: 0; - width: calc(100% + 3rem); + width: calc(100% + 1.5rem); margin: 0; z-index: 0; diff --git a/src/components/calls/group/GroupCallParticipantMenu.tsx b/src/components/calls/group/GroupCallParticipantMenu.tsx index 28666d751..a492da704 100644 --- a/src/components/calls/group/GroupCallParticipantMenu.tsx +++ b/src/components/calls/group/GroupCallParticipantMenu.tsx @@ -1,19 +1,17 @@ import type { GroupCallParticipant } from '../../../lib/secret-sauce'; import type { FC } from '../../../lib/teact/teact'; import React, { - memo, useCallback, useEffect, useState, + memo, useEffect, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { IAnchorPosition } from '../../../types'; - import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; import buildClassName from '../../../util/buildClassName'; -import buildStyle from '../../../util/buildStyle'; import useRunThrottled from '../../../hooks/useRunThrottled'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls'; import Menu from '../../ui/Menu'; @@ -28,9 +26,15 @@ const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34]; type OwnProps = { participant?: GroupCallParticipant; - closeDropdown: VoidFunction; + onCloseAnimationEnd: VoidFunction; + onClose: VoidFunction; isDropdownOpen: boolean; - anchor?: IAnchorPosition; + positionX?: 'left' | 'right'; + positionY?: 'top' | 'bottom'; + transformOriginX?: number; + transformOriginY?: number; + style?: string; + menuRef?: React.RefObject; }; type StateProps = { @@ -48,10 +52,16 @@ const SPEAKER_ICON_SIZE = 24; const GroupCallParticipantMenu: FC = ({ participant, - closeDropdown, + onCloseAnimationEnd, + onClose, isDropdownOpen, - anchor, isAdmin, + positionY, + menuRef, + positionX, + style, + transformOriginY, + transformOriginX, }) => { const { toggleGroupCallMute, @@ -75,6 +85,22 @@ const GroupCallParticipantMenu: FC = ({ isMutedByMe ? VOLUME_ZERO : ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER), ); + const [shouldPlay, setShouldPlay] = useState(false); + + const isLocalVolumeZero = localVolume === VOLUME_ZERO; + const speakerIconPlaySegment = isLocalVolumeZero ? SPEAKER_ICON_DISABLED_SEGMENT : SPEAKER_ICON_ENABLED_SEGMENT; + + useEffect(() => { + if (isDropdownOpen) return; + setShouldPlay(false); + }, [isDropdownOpen]); + + const handleSetLocalVolume = useLastCallback((volume: number) => { + setLocalVolume(volume); + const isNewLocalVolumeZero = volume === VOLUME_ZERO; + setShouldPlay(isNewLocalVolumeZero !== isLocalVolumeZero); + }); + useEffect(() => { setLocalVolume(isMutedByMe ? VOLUME_ZERO @@ -85,49 +111,49 @@ const GroupCallParticipantMenu: FC = ({ const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE); - const handleRemove = useCallback((e: React.SyntheticEvent) => { + const handleRemove = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); openDeleteUserModal(); - closeDropdown(); - }, [openDeleteUserModal, closeDropdown]); + onClose(); + }); - const handleCancelRequestToSpeak = useCallback((e: React.SyntheticEvent) => { + const handleCancelRequestToSpeak = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); requestToSpeak({ value: false, }); - closeDropdown(); - }, [requestToSpeak, closeDropdown]); + onClose(); + }); - const handleMute = useCallback((e: React.SyntheticEvent) => { + const handleMute = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); - closeDropdown(); + onClose(); if (!isAdmin) { - setLocalVolume(isMutedByMe ? GROUP_CALL_DEFAULT_VOLUME / GROUP_CALL_VOLUME_MULTIPLIER : VOLUME_ZERO); + handleSetLocalVolume(isMutedByMe ? GROUP_CALL_DEFAULT_VOLUME / GROUP_CALL_VOLUME_MULTIPLIER : VOLUME_ZERO); + } else if (shouldRaiseHand) { + handleSetLocalVolume((participant?.volume ?? GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER); } toggleGroupCallMute({ participantId: id!, value: isAdmin ? !shouldRaiseHand : !isMutedByMe, }); - }, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]); + }); - const handleOpenProfile = useCallback((e: React.SyntheticEvent) => { + const handleOpenProfile = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); toggleGroupCallPanel(); openChat({ id, }); - closeDropdown(); - }, [toggleGroupCallPanel, closeDropdown, openChat, id]); - - const isLocalVolumeZero = localVolume === VOLUME_ZERO; - const speakerIconPlaySegment = isLocalVolumeZero ? SPEAKER_ICON_DISABLED_SEGMENT : SPEAKER_ICON_ENABLED_SEGMENT; + onClose(); + }); const handleChangeVolume = (e: React.ChangeEvent) => { const value = Number(e.target.value); - setLocalVolume(value); + handleSetLocalVolume(value); + runThrottled(() => { if (value === VOLUME_ZERO) { toggleGroupCallMute({ @@ -147,11 +173,16 @@ const GroupCallParticipantMenu: FC = ({
{!isSelf && !shouldRaiseHand && (
@@ -173,6 +204,7 @@ const GroupCallParticipantMenu: FC = ({
diff --git a/src/components/calls/group/GroupCallParticipantStreams.tsx b/src/components/calls/group/GroupCallParticipantStreams.tsx deleted file mode 100644 index f5f11f0af..000000000 --- a/src/components/calls/group/GroupCallParticipantStreams.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { GroupCallParticipant } from '../../../lib/secret-sauce'; -import type { FC } from '../../../lib/teact/teact'; -import React, { - memo, useCallback, useMemo, useState, -} from '../../../lib/teact/teact'; -import { withGlobal } from '../../../global'; -import GroupCallParticipantVideo from './GroupCallParticipantVideo'; -import { selectActiveGroupCall } from '../../../global/selectors/calls'; -import buildClassName from '../../../util/buildClassName'; - -type OwnProps = { - onDoubleClick?: VoidFunction; -}; - -type StateProps = { - participants?: Record; -}; - -type SelectedVideo = { - type: 'video' | 'presentation'; - id: string; -}; - -const GroupCallParticipantStreams: FC = ({ - participants, - onDoubleClick, -}) => { - const [selectedVideo, setSelectedVideo] = useState(undefined); - const presentationParticipants = useMemo(() => { - return Object.values(participants || {}).filter((participant) => participant.hasPresentationStream); - }, [participants]); - const videoParticipants = useMemo(() => { - return Object.values(participants || {}).filter((participant) => participant.hasVideoStream); - }, [participants]); - - const totalVideoCount = videoParticipants.length + presentationParticipants.length; - // TODO replace with more adequate solution. - // There's a max of 30 videos or so right now - const columnCount = totalVideoCount <= 2 ? 1 : ( - totalVideoCount <= 6 ? 2 : ( - totalVideoCount <= 9 ? 3 : 4 - ) - ); - - const shouldSpanLastVideo = totalVideoCount === 3 || (columnCount === 2 && totalVideoCount % 2 !== 0); - - const handleClickVideo = useCallback((id: string, type: 'video' | 'presentation') => { - if (!selectedVideo || (id !== selectedVideo.id || type !== selectedVideo.type)) { - setSelectedVideo({ - id, - type, - }); - } else { - setSelectedVideo(undefined); - } - }, [selectedVideo]); - - return ( -
-
- {selectedVideo && ( - - )} - - {!selectedVideo ? presentationParticipants.map((participant) => ( - - )) : undefined} - {!selectedVideo ? videoParticipants.map((participant) => ( - - )) : undefined} -
-
- ); -}; - -export default memo(withGlobal( - (global): StateProps => { - const { participants } = selectActiveGroupCall(global) || {}; - return { - participants, - }; - }, -)(GroupCallParticipantStreams)); diff --git a/src/components/calls/group/GroupCallParticipantVideo.module.scss b/src/components/calls/group/GroupCallParticipantVideo.module.scss new file mode 100644 index 000000000..173c63718 --- /dev/null +++ b/src/components/calls/group/GroupCallParticipantVideo.module.scss @@ -0,0 +1,150 @@ +.wrapper { + position: absolute; + opacity: 1; + transform: translate(var(--x), var(--y)) scale(1); + + width: var(--width); + height: var(--height); +} + +.hidden { + opacity: 0; + transform: translate(var(--x), var(--y)) scale(0.6); +} + +.noAnimate { + transition: none; +} + +.root { + position: relative; + width: 100%; + height: 100%; + + display: flex; + border-radius: 0.625rem; + user-select: none; + + &::before { + content: ''; + display: block; + position: absolute; + inset: -0.125rem; + border-radius: 0.75rem; + background: var(--gradient-speaking); + + transform: scale(0.96); + transition: 0.25s ease-in-out transform; + } + + &::after { + content: ''; + display: block; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 55.62%, rgba(0, 0, 0, 0.5) 86.46%); + z-index: 2; + border-radius: 0.625rem; + } + + &.speaking::before { + transform: scale(1); + } +} + +.video { + width: 100%; + display: block; + object-fit: contain; + border-radius: 0.625rem; + z-index: 2; +} + +.videoFallback { + composes: video; + position: absolute; + height: 100%; + z-index: 1; +} + +.thumbnailWrapper { + position: absolute; + z-index: 0; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 0.625rem; + background: #000; +} + +.thumbnail { + object-fit: cover; + width: 100%; + height: 100%; +} + +.flipped { + transform: rotateY(180deg); +} + +.pinButton { + position: absolute; + inset-inline-end: 0.25rem; + inset-block-start: 0.25rem; + z-index: 3; + color: #FFFFFF !important; +} + +.bottomPanel { + position: absolute; + inset-block-end: 0; + inset-inline: 0; + border-end-end-radius: 0.625rem; + border-end-start-radius: 0.625rem; + + padding: 0.5rem 0.75rem; + display: flex; + align-items: center; + gap: 0.25rem; + z-index: 3; +} + +.info { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 1rem; + min-width: 0; +} + +.pinned .bottomPanel, .pinned::after { + opacity: 0; + transition: 0.25s ease-in-out opacity; +} + +.pinned:hover .bottomPanel, .pinned:hover::after { + opacity: 1; +} + +.name { + color: #FFFFFF; + font-weight: 500; + line-height: 1.125rem; + + :global(.fullName) { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 1rem; + } +} + +.status { + color: #FFFFFF; + opacity: 0.6; + line-height: 1.125rem; +} + +.icon { + margin-left: auto; +} diff --git a/src/components/calls/group/GroupCallParticipantVideo.scss b/src/components/calls/group/GroupCallParticipantVideo.scss deleted file mode 100644 index 2918592e5..000000000 --- a/src/components/calls/group/GroupCallParticipantVideo.scss +++ /dev/null @@ -1,126 +0,0 @@ -.GroupCallParticipantVideo { - border-radius: 0.75rem; - overflow: hidden; - position: relative; - max-height: 12.875rem; - width: calc(50% - 0.25rem); - /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ - transition: 0.25s ease-out width; - cursor: var(--custom-cursor, pointer); - - .thumbnail-avatar { - position: absolute; - border-radius: 0; - width: 100%; - height: 100%; - transform: scale(1.1); - - img { - filter: blur(10px); - border-radius: 0; - object-fit: cover; - } - } - - &:last-child:nth-child(odd) { - width: 100%; - } - - &::before { - box-shadow: 0 0 0 3px transparent inset; - width: 100%; - height: 100%; - position: absolute; - display: block; - content: ""; - z-index: 5; - border-radius: 0.75rem; - transition: 0.25s ease-out box-shadow; - } - - &.active::before { - box-shadow: 0px 0px 0px 3px #78ee7e inset; - } - - .back-button { - position: absolute; - z-index: 5; - top: 0.75rem; - left: 0.75rem; - background: rgba(0, 0, 0, 0.3); - border: 0; - color: white; - border-radius: 1rem; - padding: 0.25rem 0.75rem; - display: flex; - align-items: center; - gap: 0.25rem; - transition: 0.25s ease-out opacity, 0.25s ease-out background-color; - opacity: 0; - cursor: var(--custom-cursor, pointer); - outline: none !important; - - &:hover { - background: rgba(0, 0, 0, 0.4); - } - } - - video { - display: block; - width: 100%; - } - - .video { - object-fit: contain; - height: 12.5rem; - position: relative; - } - - .thumbnail-wrapper { - position: absolute; - top: 50%; - left: 50%; - z-index: 0; - width: 100%; - transform: translate(-50%, -50%) scale(1.5); - background: black; - } - - .thumbnail { - filter: blur(10px) brightness(0.5); - object-fit: cover; - } - - .info { - position: absolute; - bottom: 0; - color: #fff; - display: flex; - align-items: center; - padding: 0 0.5rem 0.25rem; - width: 100%; - height: 2rem; - background: linear-gradient(0deg, #000, transparent); - transition: 0.25s ease-out opacity; - opacity: 0; - - .name { - margin-left: 0.5rem; - } - - .last-icon { - margin-left: auto; - } - } -} - -.videos:hover .GroupCallParticipantVideo { - - .info { - opacity: 1; - } - - .back-button { - opacity: 1; - } -} diff --git a/src/components/calls/group/GroupCallParticipantVideo.tsx b/src/components/calls/group/GroupCallParticipantVideo.tsx index e09f3ef36..46857ee95 100644 --- a/src/components/calls/group/GroupCallParticipantVideo.tsx +++ b/src/components/calls/group/GroupCallParticipantVideo.tsx @@ -1,77 +1,314 @@ -import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; -import { getUserStreams, THRESHOLD } from '../../../lib/secret-sauce'; -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useCallback } from '../../../lib/teact/teact'; +import React, { + memo, useCallback, useEffect, useMemo, useRef, useState, +} from '../../../lib/teact/teact'; import { withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; +import type { VideoLayout, VideoParticipant } from './hooks/useGroupCallVideoLayout'; +import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce'; import type { ApiChat, ApiUser } from '../../../api/types'; -import { GROUP_CALL_THUMB_VIDEO_DISABLED } from '../../../config'; +import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; +import { getUserStreams, THRESHOLD } from '../../../lib/secret-sauce'; import buildClassName from '../../../util/buildClassName'; import { selectChat, selectUser } from '../../../global/selectors'; +import { animate } from '../../../util/animation'; +import { fastRaf } from '../../../util/schedulers'; +import { requestMutation } from '../../../lib/fasterdom/fasterdom'; + import useLang from '../../../hooks/useLang'; +import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; +import useMenuPosition from '../../../hooks/useMenuPosition'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useInterval from '../../../hooks/useInterval'; -import Avatar from '../../common/Avatar'; +import Button from '../../ui/Button'; +import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon'; +import FullNameTitle from '../../common/FullNameTitle'; +import GroupCallParticipantMenu from './GroupCallParticipantMenu'; -import './GroupCallParticipantVideo.scss'; +import styles from './GroupCallParticipantVideo.module.scss'; + +const VIDEO_FALLBACK_UPDATE_INTERVAL = 1000; type OwnProps = { + layout: VideoLayout; + setPinned: (participant?: VideoParticipant) => void; + pinnedVideo: VideoParticipant | undefined; + canPin: boolean; participant: TypeGroupCallParticipant; - type: 'video' | 'presentation'; - onClick?: (id: string, type: 'video' | 'presentation') => void; - isFullscreen?: boolean; + className?: string; }; type StateProps = { user?: ApiUser; chat?: ApiChat; - currentUserId?: string; - isActive?: boolean; }; const GroupCallParticipantVideo: FC = ({ - type, - onClick, + layout, + pinnedVideo, + setPinned, + canPin, + className, + participant, user, chat, - isActive, - isFullscreen, }) => { const lang = useLang(); - const handleClick = useCallback(() => { - if (onClick) { - onClick(user?.id || chat!.id, type); - } - }, [chat, onClick, type, user?.id]); + // eslint-disable-next-line no-null/no-null + const thumbnailRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const videoRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const videoFallbackRef = useRef(null); - if (!user && !chat) return undefined; + const { + x, y, width, height, noAnimate, isRemoved, + type, + } = layout; + const { + isSelf, isMutedByMe, isMuted, + } = participant; + const isPinned = pinnedVideo?.id === participant.id && pinnedVideo?.type === type; + const isSpeaking = (participant.amplitude || 0) > THRESHOLD; + const isRaiseHand = Boolean(participant.raiseHandRating); + const shouldFlipVideo = type === 'video' && participant.isSelf; + + const status = useMemo(() => { + if (isSelf) { + return lang('ThisIsYou'); + } + + if (isMutedByMe) { + return lang('VoipGroupMutedForMe'); + } + + if (isRaiseHand) { + return lang('WantsToSpeak'); + } + + if (isMuted || !isSpeaking) { + return lang('Listening'); + } + + if (participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME) { + return lang('SpeakingWithVolume', + (participant.volume / GROUP_CALL_VOLUME_MULTIPLIER).toString()) + .replace('%%', '%'); + } + + return lang('Speaking'); + }, [isSpeaking, participant.volume, lang, isSelf, isMutedByMe, isRaiseHand, isMuted]); + + const prevLayoutRef = useRef(); + if (!isRemoved) { + prevLayoutRef.current = layout; + } + const { + x: prevX, y: prevY, width: prevWidth, height: prevHeight, + } = prevLayoutRef.current || {}; + + const [currentX, currentY, currentWidth, currentHeight] = isRemoved + ? [prevX, prevY, prevWidth, prevHeight] : [x, y, width, height]; + + const [isHidden, setIsHidden] = useState(!noAnimate); const streams = getUserStreams(user?.id || chat!.id); + const actualStream = type === 'video' ? streams?.video : streams?.presentation; + const streamRef = useRef(actualStream); + if (actualStream?.active && actualStream?.getVideoTracks()[0].enabled) { + streamRef.current = actualStream; + } + const stream = streamRef.current; + + const handleInactive = useLastCallback(() => { + const video = videoRef.current; + if (!video) return; + // eslint-disable-next-line no-null/no-null + video.srcObject = null; + }); + + useEffect(() => { + stream?.addEventListener('inactive', handleInactive); + return () => { + stream?.removeEventListener('inactive', handleInactive); + }; + }, [handleInactive, stream]); + + useEffect(() => { + setIsHidden(false); + }, []); + + // When video stream is removed, the video element starts showing empty black screen. + // To avoid that, we hide the video element and show the fallback frame instead, which is constantly updated + // every VIDEO_FALLBACK_UPDATE_INTERVAL milliseconds. + useInterval(() => { + if (!stream?.active) return; + const video = videoRef.current!; + const canvas = videoFallbackRef.current!; + + requestMutation(() => { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext('2d')!.drawImage(video, 0, 0, canvas.width, canvas.height); + }); + }, VIDEO_FALLBACK_UPDATE_INTERVAL); + + useEffect(() => { + const video = videoRef.current; + const thumbnail = thumbnailRef.current; + if (!video || !thumbnail || !stream) return undefined; + + const ctx = thumbnail.getContext('2d', { alpha: false }); + if (!ctx) return undefined; + + let isDrawing = true; + requestMutation(() => { + if (!isDrawing) return; + thumbnail.width = 16; + thumbnail.height = 16; + ctx.filter = 'blur(2px)'; + + const draw = () => { + if (!isDrawing) return false; + if (!stream.active) { + return false; + } + ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, thumbnail.width, thumbnail.height); + return true; + }; + + animate(draw, fastRaf); + }); + + return () => { + isDrawing = false; + }; + }, [stream]); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + // eslint-disable-next-line no-null/no-null + const menuRef = useRef(null); + + const { + isContextMenuOpen, + contextMenuPosition, + handleContextMenu, + handleContextMenuClose, + handleContextMenuHide, + } = useContextMenuHandlers(ref, isSelf); + + const getTriggerElement = useCallback(() => ref.current, []); + + const getRootElement = useCallback( + () => ref.current!.closest('.custom-scroll, .no-scrollbar'), + [], + ); + + const getMenuElement = useCallback( + () => menuRef.current!, + [], + ); + + const getLayout = useCallback( + () => ({ withPortal: true }), + [], + ); + + const { + positionX, positionY, transformOriginX, transformOriginY, style: menuStyle, + } = useMenuPosition( + contextMenuPosition, + getTriggerElement, + getRootElement, + getMenuElement, + getLayout, + ); + + const handleClickPin = useCallback(() => { + setPinned(!isPinned ? { + id: user?.id || chat!.id, + type, + } : undefined); + }, [chat, isPinned, setPinned, type, user?.id]); return (
- {isFullscreen && ( - - )} - - {!GROUP_CALL_THUMB_VIDEO_DISABLED && ( -
-