Commit cb3bdc76 authored by Shehan Liyanage's avatar Shehan Liyanage

Merge branch 'feat/configure-project' into 'develop'

Feat/configure project

See merge request !3
parents 094e96db 8c35cf5b
......@@ -36,7 +36,8 @@
"@typescript-eslint/consistent-indexed-object-style": "warn",
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/consistent-type-imports": "warn",
"no-restricted-imports": ["warn", { "patterns": ["../*"] }]
// "no-restricted-imports": ["warn", { "patterns": ["../*"] }]
"@typescript-eslint/no-unused-vars": "warn"
},
"settings": {
"react": {
......
......@@ -32,4 +32,6 @@ typings/
*.tgz
# Yarn Integrity file
.yarn-integrity
\ No newline at end of file
.yarn-integrity
build/
\ No newline at end of file
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": false
"editor.formatOnPaste": false,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
import React from 'react'
import './App.css'
import { ThemeProvider } from '@mui/material/styles'
import { Routes, Route } from 'react-router-dom'
import VideoCall from './Pages/VideoCall'
import JoinRoom from './Pages/JoinRoom'
import CreateRoom from './Pages/CreateRoom'
import VideoCall from './Pages/VideoCall/VideoCall'
import { theme } from './Services/Utils/theme'
import { Box } from '@mui/material'
function App() {
return (
<div className='App'>
<Routes>
<Route path='/' element={<VideoCall />} />
<Route path='/join' element={<JoinRoom />} />
<Route path='/create' element={<CreateRoom />} />
</Routes>
</div>
<ThemeProvider theme={theme}>
<Box className='App'>
<Routes>
<Route path='/' element={<VideoCall />} />
<Route path='/:channel' element={<VideoCall />} />
</Routes>
</Box>
</ThemeProvider>
)
}
......
import { Box, Typography } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles'
import ControlsButtonGroup from '../ControlsButtonGroup/ControlsButtonGroup'
import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import React, { useEffect } from 'react'
interface Props {
setJoined: (x: boolean) => void
localCameraTrack: ICameraVideoTrack | null
localMicrophoneTrack: IMicrophoneAudioTrack | null
isCaptionEnable: boolean
setIsCaptionEnable: (value: boolean) => void
}
const ControlCenter = ({
setJoined,
localCameraTrack,
localMicrophoneTrack,
isCaptionEnable,
setIsCaptionEnable,
}: Props) => {
const theme: Theme = useTheme()
const [currentTime, setCurrentTime] = React.useState(new Date())
const updateTime = () => {
setCurrentTime(new Date())
}
useEffect(() => {
const interval = setInterval(updateTime, 6000)
return () => clearInterval(interval)
}, [])
return (
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Box
sx={{
width: '20%',
paddingLeft: '0px',
alignItems: 'center',
justifyContent: 'flex-start',
textAlign: 'left',
}}
>
<Typography
sx={{
fontSize: '18px',
color: theme.palette.common.white,
fontWeight: 600,
textAlign: 'left',
paddingLeft: '50px',
}}
>
{currentTime.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
})}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}
>
<ControlsButtonGroup
setJoined={setJoined}
localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack}
isCaptionEnable={isCaptionEnable}
setIsCaptionEnable={setIsCaptionEnable}
/>
</Box>
<Box
sx={{
width: '20%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: '20px',
alignItems: 'center',
}}
></Box>
</Box>
)
}
export default ControlCenter
import React, { useState } from 'react'
import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import { Box, IconButton } from '@mui/material'
import MicIcon from '@mui/icons-material/Mic'
import VideocamIcon from '@mui/icons-material/Videocam'
import { useTheme, type Theme } from '@mui/material/styles'
import ClosedCaptionIcon from '@mui/icons-material/ClosedCaption'
import PresentToAllIcon from '@mui/icons-material/PresentToAll'
import MicOffIcon from '@mui/icons-material/MicOff'
import CallEndIcon from '@mui/icons-material/CallEnd'
import VideocamOffIcon from '@mui/icons-material/VideocamOff'
interface Props {
setJoined: (x: boolean) => void
localCameraTrack: ICameraVideoTrack | null
localMicrophoneTrack: IMicrophoneAudioTrack | null
isCaptionEnable: boolean
setIsCaptionEnable: (value: boolean) => void
}
const ControlsButtonGroup = ({
setJoined,
localCameraTrack,
localMicrophoneTrack,
isCaptionEnable,
setIsCaptionEnable,
}: Props) => {
const theme: Theme = useTheme()
const [isAudioEnable, setIsAudioEnable] = useState(localMicrophoneTrack?.enabled)
const [isVideoEnabled, setIsVideoEnabled] = useState(localCameraTrack?.enabled)
const [isScreenShared, setIsScreenShared] = useState(false)
const toogleAudio = async () => {
if (isAudioEnable) {
await localMicrophoneTrack?.setEnabled(false)
setIsAudioEnable(false)
} else {
await localMicrophoneTrack?.setEnabled(true)
setIsAudioEnable(true)
}
}
const toogleVideo = async () => {
if (isVideoEnabled) {
await localCameraTrack?.setEnabled(false)
setIsVideoEnabled(false)
} else {
await localCameraTrack?.setEnabled(true)
setIsVideoEnabled(true)
}
}
const tooogleCaption = () => {
setIsCaptionEnable(!isCaptionEnable)
}
const toogleScreenShare = () => {
setIsScreenShared(!isScreenShared)
}
const handleLeaveCall = () => {
localCameraTrack?.close()
localMicrophoneTrack?.close()
setJoined(false)
}
return (
<Box sx={{ height: '100%', width: '100%' }}>
<IconButton
sx={{
borderRadius: '50%',
padding: '15px',
marginX: '15px',
backgroundColor: isAudioEnable ? 'green' : '#363739',
'&:hover': { backgroundColor: isAudioEnable ? 'green' : '#363739' },
}}
onClick={toogleAudio}
>
{isAudioEnable ? (
<MicIcon sx={{ color: theme.palette.common.white }} />
) : (
<MicOffIcon sx={{ color: theme.palette.common.white }} />
)}
</IconButton>
<IconButton
sx={{
borderRadius: '50%',
padding: '15px',
marginX: '15px',
backgroundColor: isVideoEnabled ? 'green' : '#363739',
'&:hover': { backgroundColor: isVideoEnabled ? 'green' : '#363739' },
}}
onClick={toogleVideo}
>
{isVideoEnabled ? (
<VideocamIcon sx={{ color: theme.palette.common.white }} />
) : (
<VideocamOffIcon sx={{ color: theme.palette.common.white }} />
)}
</IconButton>
<IconButton
sx={{
borderRadius: '50%',
padding: '15px',
marginX: '15px',
backgroundColor: isCaptionEnable ? '#8ab4f8' : '#363739',
'&:hover': { backgroundColor: isCaptionEnable ? '#8ab4f8' : '#363739' },
}}
onClick={tooogleCaption}
>
<ClosedCaptionIcon sx={{ color: isCaptionEnable ? 'black' : theme.palette.common.white }} />
</IconButton>
<IconButton
sx={{
borderRadius: '50%',
padding: '15px',
marginX: '15px',
backgroundColor: isScreenShared ? 'blue' : '#363739',
'&:hover': { backgroundColor: isScreenShared ? 'blue' : '#363739' },
}}
onClick={toogleScreenShare}
>
<PresentToAllIcon sx={{ color: theme.palette.common.white }} />
</IconButton>
<IconButton
sx={{
borderRadius: '40%',
padding: '15px',
paddingX: '25px',
marginX: '15px',
backgroundColor: 'red',
'&:hover': { backgroundColor: 'red' },
}}
onClick={handleLeaveCall}
>
<CallEndIcon sx={{ color: theme.palette.common.white }} />
</IconButton>
</Box>
)
}
export default ControlsButtonGroup
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Divider,
IconButton,
TextField,
Typography,
} from '@mui/material'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
import { useTheme, type Theme } from '@mui/material/styles'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
interface Props {
setJoined: (x: boolean) => void
}
const Lobby = ({ setJoined }: Props) => {
const theme: Theme = useTheme()
const navigate = useNavigate()
const [roomName, setRoomName] = useState('')
const handleJoinRoom = () => {
navigate(`/${roomName}`)
setJoined(true)
}
const handleCreateRoom = () => {
const roomName = Array.from({ length: 8 }, () => Math.random().toString(36)[2] || 0).join('')
navigate(`/${roomName}`)
setJoined(true)
}
return (
<Box
sx={{
backgroundColor: theme.palette.background.default,
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box
width={'35%'}
borderRadius={'20px'}
sx={{ backgroundColor: '#262625', display: 'flex', flexDirection: 'column' }}
>
<Box sx={{ paddingY: '20px', backgroundColor: '#363739', borderRadius: '20px 20px 0 0' }}>
<Typography
variant='h4'
sx={{
color: theme.palette.common.white,
textAlign: 'center',
fontSize: '18px',
fontWeight: '600',
}}
>
Create or Join Room 👋
</Typography>
</Box>
<Box padding={'30px'} display={'flex'} flexDirection={'column'}>
<TextField
id='username'
label='Your Name'
variant='outlined'
sx={{
backgroundColor: '#3f434a',
borderRadius: '10px',
color: theme.palette.common.white,
marginBottom: '30px',
}}
inputProps={{
style: { color: theme.palette.common.white, borderColor: theme.palette.common.white },
}}
InputLabelProps={{ style: { color: theme.palette.common.white } }}
/>
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<Box
sx={{
width: '50%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<TextField
id='room'
label='Room Name'
variant='outlined'
sx={{
backgroundColor: '#3f434a',
borderRadius: '10px',
color: theme.palette.common.white,
width: '75%',
}}
inputProps={{ style: { color: theme.palette.common.white, borderColor: 'white' } }}
InputLabelProps={{ style: { color: theme.palette.common.white } }}
onChange={(e) => setRoomName(e.target.value)}
/>
<IconButton
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.common.white,
width: '18%',
borderRadius: '10px',
height: '50px',
}}
aria-label='join room'
onClick={handleJoinRoom}
>
<ChevronRightIcon />
</IconButton>
</Box>
<Divider
orientation='vertical'
variant='middle'
sx={{ backgroundColor: theme.palette.common.white }}
flexItem
/>
<Box
sx={{ display: 'flex', justifyContent: 'end', alignItems: 'center', width: '35%' }}
>
<Button
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.common.white,
width: '90%',
borderRadius: '10px',
height: '50px',
}}
onClick={handleCreateRoom}
>
Create Room
</Button>
</Box>
</Box>
<Box sx={{ marginTop: '30px' }}>
<Accordion sx={{ backgroundColor: '#3f434a' }}>
<AccordionSummary
expandIcon={<ArrowDropDownIcon sx={{ color: theme.palette.common.white }} />}
aria-controls='accord-content'
id='accord-header'
>
<Typography
variant='h6'
sx={{ color: theme.palette.common.white, fontSize: '15px' }}
>
Are you a differently-abled person? 🦸
</Typography>
</AccordionSummary>
<AccordionDetails>
<Box>sdsd</Box>
</AccordionDetails>
</Accordion>
</Box>
</Box>
</Box>
</Box>
)
}
export default Lobby
import React, { useEffect, useRef, useState } from 'react'
import { Box } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles'
import { useRemoteUsers, usePublish, useRTCClient, useClientEvent } from 'agora-rtc-react'
import SymmetricGrid from '../SymmetricGrid/SymmetricGrid'
import PinnedGrid from '../PinnedGrid/PinnedGrid'
import type {
IAgoraRTCRemoteUser,
ICameraVideoTrack,
IMicrophoneAudioTrack,
} from 'agora-rtc-sdk-ng'
import ControlCenter from '../ControlCenter/ControlCenter'
interface Props {
setJoined: (x: boolean) => void
localCameraTrack: ICameraVideoTrack | null
localMicrophoneTrack: IMicrophoneAudioTrack | null
}
const MeetingLayout = ({ setJoined, localCameraTrack, localMicrophoneTrack }: Props) => {
const theme: Theme = useTheme()
const agoraEngine = useRTCClient()
usePublish([localMicrophoneTrack, localCameraTrack])
const remoteUsers: IAgoraRTCRemoteUser[] = useRemoteUsers()
const gridContainerRef = useRef<HTMLDivElement>(null)
const [gridContainerHeight, setGridContainerHeight] = useState(0)
const [totalUsers, setTotalUsers] = useState(0)
const [isPinned, setIsPinned] = useState(false)
const [pinnedUser, setPinnedUser] = useState<IAgoraRTCRemoteUser | null>(null)
const [isCaptionEnable, setIsCaptionEnable] = useState(false)
useEffect(() => {
setTotalUsers(remoteUsers.length + 1)
}, [remoteUsers])
useEffect(() => {
if (gridContainerRef.current) {
setGridContainerHeight(gridContainerRef.current.offsetHeight)
}
}, [gridContainerRef?.current?.offsetHeight, isCaptionEnable])
const handleUserLeft = (user: IAgoraRTCRemoteUser) => {
if (user.uid === pinnedUser?.uid) {
setIsPinned(false)
setPinnedUser(null)
}
}
useClientEvent(agoraEngine, 'user-left', handleUserLeft)
return (
<Box
sx={{
width: '100%',
height: '100%',
backgroundColor: theme.palette.background.default,
padding: '30px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: isCaptionEnable ? '70%' : '85%',
}}
ref={gridContainerRef}
>
{isPinned && pinnedUser ? (
<PinnedGrid
totalUsers={totalUsers}
gridContainerHeight={gridContainerHeight}
setIsPinned={setIsPinned}
pinnedUser={pinnedUser}
setPinnedUser={setPinnedUser}
localCameraTrack={localCameraTrack}
/>
) : (
<SymmetricGrid
totalUsers={totalUsers}
gridContainerHeight={gridContainerHeight}
isPinned={isPinned}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
localCameraTrack={localCameraTrack}
/>
)}
</Box>
{isCaptionEnable && (
<Box
sx={{
height: '10%',
padding: '20px 40px',
backgroundColor: '#363739',
borderRadius: '20px',
marginRight: '20px',
}}
></Box>
)}
<Box sx={{ height: '10%', width: '100%', display: 'flex', marginBottom: '40px' }}>
<ControlCenter
setJoined={setJoined}
localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack}
isCaptionEnable={isCaptionEnable}
setIsCaptionEnable={setIsCaptionEnable}
/>
</Box>
</Box>
)
}
export default MeetingLayout
import { Box, Button, CircularProgress, Typography } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles'
interface Props {
variant: 'joining' | 'loading' | 'error'
}
const MeetingLoading = ({ variant }: Props) => {
const theme: Theme = useTheme()
const renderComponent = (): JSX.Element => {
switch (variant) {
case 'joining':
return (
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography
variant='h3'
textAlign={'center'}
sx={{ fontSize: '18px', color: theme.palette.common.white, fontWeight: '600' }}
>
Joining the meeting...
</Typography>
</Box>
)
case 'loading':
return (
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}
>
<CircularProgress
size={'md'}
sx={{
color: theme.palette.common.white,
height: '20px',
width: '20px',
marginRight: '20px',
}}
/>
<Typography
variant='h3'
textAlign={'center'}
sx={{ fontSize: '18px', color: theme.palette.common.white, fontWeight: '600' }}
>
Loading devices
</Typography>
</Box>
</Box>
)
case 'error':
return (
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Box
sx={{
backgroundColor: '#262625',
borderRadius: '20px',
padding: '20px 40px',
width: '30%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography
variant='h3'
textAlign={'center'}
sx={{
fontSize: '18px',
color: theme.palette.common.white,
fontWeight: '600',
marginTop: '10px',
}}
>
Unexpected error occurred. Please try reloading the page. 😞
</Typography>
<Button
variant='text'
sx={{
color: theme.palette.primary.main,
borderRadius: '10px',
padding: '10px 20px',
marginTop: '10px',
fontWeight: '600',
}}
onClick={() => window.location.reload()}
>
Reload
</Button>
</Box>
</Box>
)
default:
return <></>
}
}
return renderComponent()
}
export default MeetingLoading
import React, { useEffect } from 'react'
import type { IAgoraRTCRemoteUser, ICameraVideoTrack } 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
localCameraTrack: ICameraVideoTrack | null
}
const PinnedGrid = ({
totalUsers,
gridContainerHeight,
setIsPinned,
pinnedUser,
setPinnedUser,
localCameraTrack,
}: 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}
localCameraTrack={localCameraTrack}
/>
{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 { Box, Grid } from '@mui/material'
import useMediaQuery from '@mui/material/useMediaQuery'
import { useRemoteUsers } from 'agora-rtc-react'
import { useTheme, type Theme } from '@mui/material/styles'
import type { IAgoraRTCRemoteUser, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import UserCard from '../UserCard/UserCard'
interface Props {
totalUsers: number
gridContainerHeight: number
isPinned: boolean
setIsPinned: (value: boolean) => void
setPinnedUser: (value: IAgoraRTCRemoteUser | null) => void
localCameraTrack: ICameraVideoTrack | null
}
const SymmetricGrid = ({
totalUsers,
gridContainerHeight,
isPinned,
setIsPinned,
setPinnedUser,
localCameraTrack,
}: Props) => {
const theme: Theme = useTheme()
const isSm = useMediaQuery(theme.breakpoints.up('sm'))
const isMd = useMediaQuery(theme.breakpoints.up('md'))
const isLg = useMediaQuery(theme.breakpoints.up('lg'))
const remoteUsers = useRemoteUsers()
const [itemOffset, setItemOffset] = useState(12)
const [itemHeight, setItemHeight] = useState(230)
const [numOfItems, setNumOfItems] = useState(1)
const maxUsersDisplay = isLg ? 12 : isMd ? 9 : isSm ? 4 : 2
useEffect(() => {
setNumOfItems(totalUsers > maxUsersDisplay ? maxUsersDisplay : totalUsers)
}, [totalUsers])
useEffect(() => {
switch (numOfItems) {
case 1:
setItemOffset(12)
setItemHeight(gridContainerHeight)
break
case 2:
setItemOffset(isSm ? 6 : 12)
setItemHeight(isSm ? gridContainerHeight : (gridContainerHeight - 16) / 2)
break
case 3:
setItemOffset(isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isMd
? gridContainerHeight
: isSm
? (gridContainerHeight - 16) / 2
: (gridContainerHeight - 32) / 3,
)
break
case 4:
setItemOffset(isSm ? 6 : 12)
setItemHeight(isSm ? (gridContainerHeight - 16) / 2 : (gridContainerHeight - 16 * 3) / 4)
break
case 5:
setItemOffset(isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isMd
? (gridContainerHeight - 16) / 2
: isSm
? (gridContainerHeight - 16 * 2) / 3
: (gridContainerHeight - 32 * 4) / 5,
)
break
case 6:
setItemOffset(isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isMd
? (gridContainerHeight - 16) / 2
: isSm
? (gridContainerHeight - 16 * 2) / 3
: (gridContainerHeight - 32 * 5) / 6,
)
break
case 7:
setItemOffset(isLg ? 3 : isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isLg
? (gridContainerHeight - 16) / 2
: isMd
? (gridContainerHeight - 16 * 2) / 3
: isSm
? (gridContainerHeight - 16 * 3) / 4
: (gridContainerHeight - 32 * 6) / 7,
)
break
case 8:
setItemOffset(isLg ? 3 : isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isLg
? (gridContainerHeight - 16) / 2
: isMd
? (gridContainerHeight - 16 * 2) / 3
: isSm
? (gridContainerHeight - 16 * 3) / 4
: (gridContainerHeight - 32 * 7) / 8,
)
break
case 9:
setItemOffset(isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isMd
? (gridContainerHeight - 16 * 2) / 3
: isSm
? (gridContainerHeight - 16 * 4) / 5
: (gridContainerHeight - 32 * 8) / 9,
)
break
default:
setItemOffset(isLg ? 3 : isMd ? 4 : isSm ? 6 : 12)
setItemHeight(
isLg ? (gridContainerHeight - 16 * 2) / 3 : (gridContainerHeight - 16 * 3) / 4,
)
}
}, [totalUsers, isSm, isMd, isLg, gridContainerHeight, numOfItems])
return (
<Box sx={{ display: 'flex', width: '100%' }}>
<Grid container spacing={3} width={'100%'} padding={0}>
<Grid item xs={itemOffset}>
<UserCard
type={'local'}
itemHeight={itemHeight}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
localCameraTrack={localCameraTrack}
/>
</Grid>
{remoteUsers.map((remoteUser, index) => (
<Grid item xs={itemOffset} key={index}>
<UserCard
type={'remote'}
remoteUser={remoteUser}
itemHeight={itemHeight}
setIsPinned={setIsPinned}
setPinnedUser={setPinnedUser}
/>
</Grid>
))}
</Grid>
</Box>
)
}
export default SymmetricGrid
import { Box, Button, IconButton, Popover } from '@mui/material'
import MoreVertIcon from '@mui/icons-material/MoreVert'
import { LocalVideoTrack, RemoteUser } from 'agora-rtc-react'
import type { IAgoraRTCRemoteUser, ICameraVideoTrack } 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
localCameraTrack?: ICameraVideoTrack | null
}
const UserCard = ({
type,
remoteUser,
itemHeight,
setIsPinned,
setPinnedUser,
localCameraTrack,
}: Props) => {
const theme: Theme = useTheme()
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
import React from 'react'
const index = () => {
return <div>Create Room</div>
}
export default index
import React from 'react'
const index = () => {
return <div>Join Room</div>
}
export default index
import { useState } from 'react'
import AgoraRTC from 'agora-rtc-sdk-ng'
import { AgoraRTCProvider, useRTCClient } from 'agora-rtc-react'
import { AgoraManager } from '../../Services/AgoraManager/AgoraManager'
import Lobby from '../../Components/Lobby/Lobby'
import { Box } from '@mui/material'
const VideoCall = () => {
const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: 'vp8', mode: 'rtc' }))
const [joined, setJoined] = useState(false)
return (
<Box sx={{ height: '100vh', width: '100vw', overflow: 'hidden' }}>
{joined ? (
<AgoraRTCProvider client={agoraEngine}>
<AgoraManager setJoined={setJoined}>
<div>asas</div>
</AgoraManager>
</AgoraRTCProvider>
) : (
<Lobby setJoined={setJoined} />
)}
</Box>
)
}
export default VideoCall
import React from 'react'
const index = () => {
return <div>index</div>
}
export default index
import React, { createContext, useContext, useEffect } from 'react'
import { useJoin, useLocalCameraTrack, useLocalMicrophoneTrack } from 'agora-rtc-react'
import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import { Box } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles'
import MeetingLoading from '../../Components/MeetingLoading/MeetingLoading'
import MeetingLayout from '../../Components/MeetingLayout/MeetingLayout'
import { useParams } from 'react-router-dom'
interface AgoraContextType {
localCameraTrack: ICameraVideoTrack | null
localMicrophoneTrack: IMicrophoneAudioTrack | null
children: React.ReactNode
}
const AgoraContext = createContext<AgoraContextType | null>(null)
export const AgoraProvider: React.FC<AgoraContextType> = ({
children,
localCameraTrack,
localMicrophoneTrack,
}) => (
<AgoraContext.Provider value={{ localCameraTrack, localMicrophoneTrack, children }}>
{children}
</AgoraContext.Provider>
)
export const useAgoraContext = () => {
const context = useContext(AgoraContext)
if (!context) throw new Error('useAgoraContext must be used within an AgoraProvider')
return context
}
export const AgoraManager = ({
setJoined,
children,
}: {
setJoined: (x: boolean) => void
children: React.ReactNode
}) => {
const theme: Theme = useTheme()
let { channel } = useParams<{ channel: string }>()
if (!channel) {
channel = Array.from({ length: 8 }, () => Math.random().toString(36)[2] || 0).join('')
}
const {
data: uid,
error: joinError,
isConnected: isJoined,
isLoading: isJoining,
} = useJoin({
appid: '3af4648782de46ddbf90005f7a68f206',
channel,
token: null,
uid: null,
})
const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack()
const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophoneTrack()
useEffect(() => {
localStorage.setItem('myUid', uid.toString())
}, [uid])
useEffect(() => {
return () => {
localCameraTrack?.close()
localMicrophoneTrack?.close()
}
}, [])
return (
<Box
sx={{
width: '100%',
height: '100%',
backgroundColor: theme.palette.background.default,
display: 'flex',
}}
>
{isJoining ? (
<MeetingLoading variant='joining' />
) : isLoadingCam || isLoadingMic ? (
<MeetingLoading variant='loading' />
) : isJoined ? (
<AgoraProvider
localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack}
>
<MeetingLayout
setJoined={setJoined}
localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack}
/>
</AgoraProvider>
) : joinError ? (
<MeetingLoading variant='error' />
) : null}
</Box>
)
}
export default AgoraManager
import { createTheme } from '@mui/material/styles'
export const theme = createTheme({
palette: {
common: {
white: '#fff',
black: '#000',
},
primary: {
main: '#845695',
dark: '#19857b',
},
secondary: {
main: '#1a1a1a',
dark: '#363739',
},
error: {
main: '#f44336',
},
background: {
default: '#1a1a1a',
},
},
})
body,
html {
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
......@@ -9,3 +16,15 @@ body {
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
#wrapper {
max-width: 940px;
margin: 0 auto;
padding: 0 5%;
clear: both;
}
video {
border-radius: 15px;
object-fit: cover;
}
......@@ -1314,10 +1314,10 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17"
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
"@emotion/react@^11.11.3":
version "11.11.3"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.3.tgz#96b855dc40a2a55f52a72f518a41db4f69c31a25"
integrity sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==
"@emotion/react@^11.11.4":
version "11.11.4"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d"
integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.11.0"
......@@ -1763,35 +1763,42 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@mui/base@5.0.0-beta.37":
version "5.0.0-beta.37"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.37.tgz#0e7e0f28402391fcfbb05476d5acc6c4f2d817b1"
integrity sha512-/o3anbb+DeCng8jNsd3704XtmmLDZju1Fo8R2o7ugrVtPQ/QpcqddwKNzKPZwa0J5T8YNW3ZVuHyQgbTnQLisQ==
"@mui/base@5.0.0-beta.39":
version "5.0.0-beta.39"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.39.tgz#9b8bab9d292e78721565197bead5050a79881194"
integrity sha512-puyUptF7VJ+9/dMIRLF+DLR21cWfvejsA6OnatfJfqFp8aMhya7xQtvYLEfCch6ahvFZvNC9FFEGGR+qkgFjUg==
dependencies:
"@babel/runtime" "^7.23.9"
"@floating-ui/react-dom" "^2.0.8"
"@mui/types" "^7.2.13"
"@mui/utils" "^5.15.11"
"@mui/utils" "^5.15.13"
"@popperjs/core" "^2.11.8"
clsx "^2.1.0"
prop-types "^15.8.1"
"@mui/core-downloads-tracker@^5.15.11":
version "5.15.11"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.11.tgz#dcaf6156880e81e4547237fb781700485453e964"
integrity sha512-JVrJ9Jo4gyU707ujnRzmE8ABBWpXd6FwL9GYULmwZRtfPg89ggXs/S3MStQkpJ1JRWfdLL6S5syXmgQGq5EDAw==
"@mui/core-downloads-tracker@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.13.tgz#f753bec8994b5defe4f62832a8a9ed14b5cb2d16"
integrity sha512-ERsk9EWpiitSiKnmUdFJGshtFk647l4p7r+mjRWe/F1l5kT1NTTKkaeDLcK3/lsy0udXjMgcG0bNwzbYBdDdhQ==
"@mui/material@^5.15.11":
version "5.15.11"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.11.tgz#4f42ee30443699ffb5836029c6d8464154eca603"
integrity sha512-FA3eEuEZaDaxgN3CgfXezMWbCZ4VCeU/sv0F0/PK5n42qIgsPVD6q+j71qS7/62sp6wRFMHtDMpXRlN+tT/7NA==
"@mui/icons-material@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.13.tgz#8eabb372e64cb4dd5ef4f02df670543fa34bf360"
integrity sha512-I7CioMQKBPaKyGgcE9i8+1dgzAmox5a/0wZ0E9sIxm7PzG5KJZRRJkdK4oDT4HfYRGv61KjcHEeqH48pht1dvQ==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/material@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.13.tgz#ba4414d90075321d631a6ecfaad69b34b995cea0"
integrity sha512-E+QisOJcIzTTyeJ0o3lgYMcyrmCydb2S4cn9vTtGpIB9uR6fQ6La3dIGsXgYEGyeOB9YkWzQbNzYzvyODGEWKA==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/base" "5.0.0-beta.37"
"@mui/core-downloads-tracker" "^5.15.11"
"@mui/system" "^5.15.11"
"@mui/base" "5.0.0-beta.39"
"@mui/core-downloads-tracker" "^5.15.13"
"@mui/system" "^5.15.13"
"@mui/types" "^7.2.13"
"@mui/utils" "^5.15.11"
"@mui/utils" "^5.15.13"
"@types/react-transition-group" "^4.4.10"
clsx "^2.1.0"
csstype "^3.1.3"
......@@ -1799,13 +1806,13 @@
react-is "^18.2.0"
react-transition-group "^4.4.5"
"@mui/private-theming@^5.15.11":
version "5.15.11"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.11.tgz#4b9289b56b1ae0beb84e47bc9952f927b6e175ae"
integrity sha512-jY/696SnSxSzO1u86Thym7ky5T9CgfidU3NFJjguldqK4f3Z5S97amZ6nffg8gTD0HBjY9scB+4ekqDEUmxZOA==
"@mui/private-theming@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.13.tgz#04c8c8a6f2e6a67e4cc3aecb9375cc23df1a6f23"
integrity sha512-j5Z2pRi6talCunIRIzpQERSaHwLd5EPdHMwIKDVCszro1RAzRZl7WmH68IMCgQmJMeglr+FalqNuq048qptGAg==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/utils" "^5.15.11"
"@mui/utils" "^5.15.13"
prop-types "^15.8.1"
"@mui/styled-engine@^5.15.11":
......@@ -1818,16 +1825,16 @@
csstype "^3.1.3"
prop-types "^15.8.1"
"@mui/system@^5.15.11":
version "5.15.11"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.11.tgz#19cf1974f82f1dd38be1f162034efecadd765733"
integrity sha512-9j35suLFq+MgJo5ktVSHPbkjDLRMBCV17NMBdEQurh6oWyGnLM4uhU4QGZZQ75o0vuhjJghOCA1jkO3+79wKsA==
"@mui/system@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.13.tgz#dd86dbbebf92e4afdf0fa01afdae28598745ba4c"
integrity sha512-eHaX3sniZXNWkxX0lmcLxROhQ5La0HkOuF7zxbSdAoHUOk07gboQYmF6hSJ/VBFx/GLanIw67FMTn88vc8niLg==
dependencies:
"@babel/runtime" "^7.23.9"
"@mui/private-theming" "^5.15.11"
"@mui/private-theming" "^5.15.13"
"@mui/styled-engine" "^5.15.11"
"@mui/types" "^7.2.13"
"@mui/utils" "^5.15.11"
"@mui/utils" "^5.15.13"
clsx "^2.1.0"
csstype "^3.1.3"
prop-types "^15.8.1"
......@@ -1837,10 +1844,10 @@
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.13.tgz#d1584912942f9dc042441ecc2d1452be39c666b8"
integrity sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==
"@mui/utils@^5.15.11":
version "5.15.11"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.11.tgz#a71804d6d6025783478fd1aca9afbf83d9b789c7"
integrity sha512-D6bwqprUa9Stf8ft0dcMqWyWDKEo7D+6pB1k8WajbqlYIRA8J8Kw9Ra7PSZKKePGBGWO+/xxrX1U8HpG/aXQCw==
"@mui/utils@^5.15.13":
version "5.15.13"
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.13.tgz#4adfed6c585a6787f1f0d7d1fadb9ff0f7ddb2bd"
integrity sha512-qNlR9FLEhORC4zVZ3fzF48213EhP/92N71AcFbhHN73lPJjAbq9lUv+71P7uEdRHdrrOlm8+1zE8/OBy6MUqdg==
dependencies:
"@babel/runtime" "^7.23.9"
"@types/prop-types" "^15.7.11"
......@@ -2861,6 +2868,23 @@ agent-base@6:
dependencies:
debug "4"
agora-rtc-react@2.0.0-beta.0:
version "2.0.0-beta.0"
resolved "https://registry.yarnpkg.com/agora-rtc-react/-/agora-rtc-react-2.0.0-beta.0.tgz#3c4336f22439e96096b61b79ad1a988eddefd7e9"
integrity sha512-oyNjEy/H3C7n7ZO3HaLd3av5UMGP9oqmJ0EceOYB0JLx5bJYmAplMyx05ku+eW+GBzH+bm1UexJgo/Wzl9DQDQ==
agora-rtc-sdk-ng@4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.18.2.tgz#86b0b7fd6763aef776f1d6aaa13c487062ee11f7"
integrity sha512-tvyuXHVL15Eo0lK3wwM3rGPKGPeGYuarAwCs5oyveht1HZO9z3w6vra3d/q3eOCYpvQscCW7P0QNfUIAQTs4RQ==
dependencies:
agora-rte-extension "^1.2.3"
agora-rte-extension@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/agora-rte-extension/-/agora-rte-extension-1.2.4.tgz#6ba1742b16191ea9eba69376b067d4b2e0fe9d34"
integrity sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
......@@ -8538,6 +8562,13 @@ react-scripts@5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react-toastify@^10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.4.tgz#6ecdbbf923a07fc45850e69b0566efc7bf733283"
integrity sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==
dependencies:
clsx "^2.1.0"
react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
......
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