Commit e6ce68d9 authored by Thisara Kavinda's avatar Thisara Kavinda

feat: implemented the pinned grid

parent 16cb267f
...@@ -6,21 +6,28 @@ import { ...@@ -6,21 +6,28 @@ import {
useLocalMicrophoneTrack, useLocalMicrophoneTrack,
useRemoteUsers, useRemoteUsers,
usePublish, usePublish,
useRTCClient,
useClientEvent,
} from 'agora-rtc-react' } from 'agora-rtc-react'
import SymmetricGrid from '../SymmetricGrid/SymmetricGrid' import SymmetricGrid from '../SymmetricGrid/SymmetricGrid'
import PinnedGrid from '../PinnedGrid/PinnedGrid'
import type { IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
const MeetingLayout = () => { const MeetingLayout = () => {
const theme: Theme = useTheme() const theme: Theme = useTheme()
const agoraEngine = useRTCClient()
const { localCameraTrack } = useLocalCameraTrack() const { localCameraTrack } = useLocalCameraTrack()
const { localMicrophoneTrack } = useLocalMicrophoneTrack() const { localMicrophoneTrack } = useLocalMicrophoneTrack()
usePublish([localMicrophoneTrack, localCameraTrack]) usePublish([localMicrophoneTrack, localCameraTrack])
const remoteUsers = useRemoteUsers() const remoteUsers: IAgoraRTCRemoteUser[] = useRemoteUsers()
const gridContainerRef = useRef<HTMLDivElement>(null) const gridContainerRef = useRef<HTMLDivElement>(null)
const [gridContainerHeight, setGridContainerHeight] = useState(0) const [gridContainerHeight, setGridContainerHeight] = useState(0)
const [totalUsers, setTotalUsers] = useState(20) const [totalUsers, setTotalUsers] = useState(0)
const [isPinned, setIsPinned] = useState(false)
const [pinnedUser, setPinnedUser] = useState<IAgoraRTCRemoteUser | null>(null)
useEffect(() => { useEffect(() => {
setTotalUsers(remoteUsers.length + 1) setTotalUsers(remoteUsers.length + 1)
...@@ -32,6 +39,15 @@ const MeetingLayout = () => { ...@@ -32,6 +39,15 @@ const MeetingLayout = () => {
} }
}, [gridContainerRef?.current?.offsetHeight]) }, [gridContainerRef?.current?.offsetHeight])
const handleUserLeft = (user: IAgoraRTCRemoteUser) => {
if (user.uid === pinnedUser?.uid) {
setIsPinned(false)
setPinnedUser(null)
}
}
useClientEvent(agoraEngine, 'user-left', handleUserLeft)
return ( return (
<Box <Box
sx={{ sx={{
...@@ -52,12 +68,23 @@ const MeetingLayout = () => { ...@@ -52,12 +68,23 @@ const MeetingLayout = () => {
}} }}
ref={gridContainerRef} ref={gridContainerRef}
> >
{isPinned && pinnedUser ? (
<PinnedGrid
totalUsers={totalUsers}
gridContainerHeight={gridContainerHeight}
setIsPinned={setIsPinned}
pinnedUser={pinnedUser}
setPinnedUser={setPinnedUser}
/>
) : (
<SymmetricGrid <SymmetricGrid
totalUsers={totalUsers} totalUsers={totalUsers}
setTotalUsers={setTotalUsers}
gridContainerHeight={gridContainerHeight} gridContainerHeight={gridContainerHeight}
setGridContainerHeight={setGridContainerHeight} isPinned={isPinned}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
/> />
)}
</Box> </Box>
</Box> </Box>
) )
......
import React, { useEffect } from 'react'
import type { IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import { Box, Button, IconButton, Popover, Stack } from '@mui/material'
import useMediaQuery from '@mui/material/useMediaQuery'
import { RemoteUser, useRemoteUsers } from 'agora-rtc-react'
import { useTheme, type Theme } from '@mui/material/styles'
import UserCard from '../UserCard/UserCard'
import MoreVertIcon from '@mui/icons-material/MoreVert'
interface Props {
totalUsers: number
gridContainerHeight: number
setIsPinned: (value: boolean) => void
pinnedUser: IAgoraRTCRemoteUser
setPinnedUser: (value: IAgoraRTCRemoteUser | null) => void
}
const PinnedGrid = ({
totalUsers,
gridContainerHeight,
setIsPinned,
pinnedUser,
setPinnedUser,
}: Props) => {
const theme: Theme = useTheme()
const isSm = useMediaQuery(theme.breakpoints.up('sm'))
const remoteUsers = useRemoteUsers()
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
const [itemHeight, setItemHeight] = React.useState((gridContainerHeight - 16 * 3) / 4)
const open = Boolean(anchorEl)
const menuId = open ? 'simple-popover' : undefined
const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleCloseMenu = () => {
setAnchorEl(null)
}
const handleUnpinUser = () => {
setIsPinned(false)
setPinnedUser(null)
setAnchorEl(null)
}
useEffect(() => {
setItemHeight((gridContainerHeight - 16 * 2) / 3)
}, [gridContainerHeight])
return (
<Box
sx={{
display: 'flex',
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between',
height: '100%',
}}
>
<Box
sx={{
width: isSm ? '65%' : '100%',
backgroundColor: '#262625',
height: '100%',
borderRadius: '15px',
marginX: '10px',
position: 'relative',
}}
>
<RemoteUser
user={pinnedUser}
playVideo={true}
playAudio={true}
style={{ width: '100%', height: '100%', borderRadius: '15px' }}
/>
<IconButton
aria-lable='Menu'
sx={{
position: 'absolute',
top: '5%',
right: '5%',
borderRadius: '50%',
padding: '10px',
zIndex: 100,
pointerEvents: 'auto',
'&:hover': { backgroundColor: '#363739', opacity: 0.8 },
}}
onClick={handleClickMenu}
>
<MoreVertIcon />
</IconButton>
<Popover
id={menuId}
open={open}
anchorEl={anchorEl}
onClose={handleCloseMenu}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Box
sx={{
padding: '5px 20px',
display: 'flex',
flexDirection: 'columnÍ',
backgroundColor: '#363739',
width: '150px',
}}
>
<Button
sx={{
color: theme.palette.common.white,
width: '100%',
textAlign: 'left',
justifyContent: 'start',
textTransform: 'none',
}}
onClick={handleUnpinUser}
>
Unpin User
</Button>
</Box>
</Popover>
</Box>
{isSm && (
<Box
sx={{
width: '30%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}}
>
<Stack spacing={4} sx={{ width: '100%', height: 'fit-content' }}>
<UserCard
type={'local'}
itemHeight={itemHeight}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
/>
{remoteUsers.length > 1 &&
remoteUsers
.filter((user) => user.uid !== pinnedUser.uid)
.map((user) => (
<UserCard
key={user.uid}
type={'remote'}
remoteUser={user}
itemHeight={itemHeight}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
/>
))}
</Stack>
</Box>
)}
</Box>
)
}
export default PinnedGrid
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Box, Grid } from '@mui/material' import { Box, Grid } from '@mui/material'
import useMediaQuery from '@mui/material/useMediaQuery' import useMediaQuery from '@mui/material/useMediaQuery'
import { LocalVideoTrack, RemoteUser, useLocalCameraTrack, useRemoteUsers } from 'agora-rtc-react' import { useRemoteUsers } from 'agora-rtc-react'
import { useTheme, type Theme } from '@mui/material/styles' import { useTheme, type Theme } from '@mui/material/styles'
import type { IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import UserCard from '../UserCard/UserCard'
interface Props { interface Props {
totalUsers: number totalUsers: number
setTotalUsers: (value: number) => void
gridContainerHeight: number gridContainerHeight: number
setGridContainerHeight: (value: number) => void isPinned: boolean
setIsPinned: (value: boolean) => void
setPinnedUser: (value: IAgoraRTCRemoteUser | null) => void
} }
const SymmetricGrid = ({ const SymmetricGrid = ({
totalUsers, totalUsers,
setTotalUsers,
gridContainerHeight, gridContainerHeight,
setGridContainerHeight, isPinned,
setIsPinned,
setPinnedUser,
}: Props) => { }: Props) => {
const theme: Theme = useTheme() const theme: Theme = useTheme()
const isSm = useMediaQuery(theme.breakpoints.up('sm')) const isSm = useMediaQuery(theme.breakpoints.up('sm'))
const isMd = useMediaQuery(theme.breakpoints.up('md')) const isMd = useMediaQuery(theme.breakpoints.up('md'))
const isLg = useMediaQuery(theme.breakpoints.up('lg')) const isLg = useMediaQuery(theme.breakpoints.up('lg'))
const { localCameraTrack } = useLocalCameraTrack()
const remoteUsers = useRemoteUsers() const remoteUsers = useRemoteUsers()
const [itemOffset, setItemOffset] = useState(12) const [itemOffset, setItemOffset] = useState(12)
...@@ -31,10 +34,6 @@ const SymmetricGrid = ({ ...@@ -31,10 +34,6 @@ const SymmetricGrid = ({
const maxUsersDisplay = isLg ? 12 : isMd ? 9 : isSm ? 4 : 2 const maxUsersDisplay = isLg ? 12 : isMd ? 9 : isSm ? 4 : 2
useEffect(() => {
setTotalUsers(remoteUsers.length + 1)
}, [remoteUsers])
useEffect(() => { useEffect(() => {
setNumOfItems(totalUsers > maxUsersDisplay ? maxUsersDisplay : totalUsers) setNumOfItems(totalUsers > maxUsersDisplay ? maxUsersDisplay : totalUsers)
}, [totalUsers]) }, [totalUsers])
...@@ -129,36 +128,22 @@ const SymmetricGrid = ({ ...@@ -129,36 +128,22 @@ const SymmetricGrid = ({
<Box sx={{ display: 'flex', width: '100%' }}> <Box sx={{ display: 'flex', width: '100%' }}>
<Grid container spacing={3} width={'100%'} padding={0}> <Grid container spacing={3} width={'100%'} padding={0}>
<Grid item xs={itemOffset}> <Grid item xs={itemOffset}>
<Box <UserCard
sx={{ type={'local'}
backgroundColor: '#262625', itemHeight={itemHeight}
height: `${itemHeight}px`, setIsPinned={setIsPinned}
borderRadius: '15px', setPinnedUser={setPinnedUser}
}}
>
<LocalVideoTrack
track={localCameraTrack}
play={true}
style={{ width: '100%', height: '100%', borderRadius: '20%' }}
/> />
</Box>
</Grid> </Grid>
{remoteUsers.map((remoteUser, index) => ( {remoteUsers.map((remoteUser, index) => (
<Grid item xs={itemOffset} key={index}> <Grid item xs={itemOffset} key={index}>
<Box <UserCard
sx={{ type={'remote'}
backgroundColor: '#262625', remoteUser={remoteUser}
height: `${itemHeight}px`, itemHeight={itemHeight}
borderRadius: '15px', setIsPinned={setIsPinned}
}} setPinnedUser={setPinnedUser}
>
<RemoteUser
user={remoteUser}
playVideo={true}
playAudio={true}
style={{ width: '100%', height: '100%', borderRadius: '15px' }}
/> />
</Box>
</Grid> </Grid>
))} ))}
</Grid> </Grid>
......
import { Box, Button, IconButton, Popover } from '@mui/material'
import MoreVertIcon from '@mui/icons-material/MoreVert'
import { LocalVideoTrack, RemoteUser, useLocalCameraTrack } from 'agora-rtc-react'
import type { IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import { useTheme, type Theme } from '@mui/material/styles'
import React, { useEffect } from 'react'
interface Props {
type: 'remote' | 'local'
itemHeight: number
setIsPinned: (value: boolean) => void
setPinnedUser: (value: IAgoraRTCRemoteUser | null) => void
remoteUser?: IAgoraRTCRemoteUser
}
const UserCard = ({ type, remoteUser, itemHeight, setIsPinned, setPinnedUser }: Props) => {
const theme: Theme = useTheme()
const { localCameraTrack } = useLocalCameraTrack()
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
const [isLocal, setIsLocal] = React.useState<boolean>(type === 'local')
const open = Boolean(anchorEl)
const menuId = open ? 'simple-popover' : undefined
useEffect(() => {
setIsLocal(type === 'local')
}, [type])
const handleClickMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}
const handleCloseMenu = () => {
setAnchorEl(null)
}
const handlePinUser = () => {
setIsPinned(true)
setPinnedUser(remoteUser ?? null)
setAnchorEl(null)
}
return (
<Box
sx={{
backgroundColor: '#262625',
height: `${itemHeight}px`,
borderRadius: '15px',
width: '100%',
position: 'relative',
overflow: 'hidden',
}}
>
{isLocal ? (
<LocalVideoTrack
track={localCameraTrack}
play={true}
style={{ width: '100%', height: '100%', borderRadius: '20%' }}
/>
) : (
<RemoteUser
user={remoteUser}
playVideo={true}
playAudio={true}
style={{ width: '100%', height: '100%', borderRadius: '15px' }}
/>
)}
{!isLocal && (
<>
<IconButton
aria-lable='Menu'
sx={{
position: 'absolute',
top: '5%',
right: '5%',
borderRadius: '50%',
padding: '10px',
zIndex: 100,
pointerEvents: 'auto',
'&:hover': { backgroundColor: '#363739', opacity: 0.8 },
}}
onClick={handleClickMenu}
>
<MoreVertIcon />
</IconButton>
<Popover
id={menuId}
open={open}
anchorEl={anchorEl}
onClose={handleCloseMenu}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Box
sx={{
padding: '5px 20px',
display: 'flex',
flexDirection: 'columnÍ',
backgroundColor: '#363739',
width: '150px',
}}
>
<Button
sx={{
color: theme.palette.common.white,
width: '100%',
textAlign: 'left',
justifyContent: 'start',
textTransform: 'none',
}}
onClick={handlePinUser}
>
Pin User
</Button>
</Box>
</Popover>
</>
)}
</Box>
)
}
export default UserCard
...@@ -5,7 +5,7 @@ import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng' ...@@ -5,7 +5,7 @@ import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles' import { useTheme, type Theme } from '@mui/material/styles'
import MeetingLoading from '../../Components/MeetingLoading/MeetingLoading' import MeetingLoading from '../../Components/MeetingLoading/MeetingLoading'
import MeetingGrid from '../../Components/MeetingLayout/MeetingLayout' import MeetingLayout from '../../Components/MeetingLayout/MeetingLayout'
interface AgoraContextType { interface AgoraContextType {
localCameraTrack: ICameraVideoTrack | null localCameraTrack: ICameraVideoTrack | null
...@@ -73,7 +73,7 @@ export const AgoraManager = ({ children }: { children: React.ReactNode }) => { ...@@ -73,7 +73,7 @@ export const AgoraManager = ({ children }: { children: React.ReactNode }) => {
localCameraTrack={localCameraTrack} localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack} localMicrophoneTrack={localMicrophoneTrack}
> >
<MeetingGrid /> <MeetingLayout />
</AgoraProvider> </AgoraProvider>
) : joinError ? ( ) : joinError ? (
<MeetingLoading variant='error' /> <MeetingLoading variant='error' />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment