diff --git a/package-lock.json b/package-lock.json index 05ba401..d99ad76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,11 +24,13 @@ "lucide-react": "^0.541.0", "mediasoup-client": "3.17.0", "mic-check": "^1.1.0", + "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", "react-router-dom": "^7.8.2", "socket.io-client": "^4.8.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.12", "uniqid": "^5.4.0", @@ -6167,6 +6169,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -6878,6 +6890,16 @@ } } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 0fd8601..17f0324 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,13 @@ "lucide-react": "^0.541.0", "mediasoup-client": "3.17.0", "mic-check": "^1.1.0", + "next-themes": "^0.4.6", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", "react-router-dom": "^7.8.2", "socket.io-client": "^4.8.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.12", "uniqid": "^5.4.0", diff --git a/src/app.tsx b/src/app.tsx index effe064..19c35ad 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,21 +1,19 @@ -import { ToastContainer, ToastProvider } from './packages/toast'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import Home from './pages/home'; import Room from './pages/room'; import { ThemeProvider } from './providers/theme-provider'; +import { Toaster } from './components/ui/sonner'; const App = () => { return ( - - - - } /> - } /> - - - - + + + } /> + } /> + + + ); }; diff --git a/src/components/home/call-to-action.tsx b/src/components/home/call-to-action.tsx index 5f907b2..f6daa78 100644 --- a/src/components/home/call-to-action.tsx +++ b/src/components/home/call-to-action.tsx @@ -27,9 +27,9 @@ const CallToAction = () => {
diff --git a/src/components/home/footer.tsx b/src/components/home/footer.tsx index f5d2b20..c717f94 100644 --- a/src/components/home/footer.tsx +++ b/src/components/home/footer.tsx @@ -14,16 +14,31 @@ const Footer = () => {
- + Documentation - + API - + Support - + GitHub
diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index 9415258..d1e8d07 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -23,7 +23,7 @@ const Hero = () => {

The future of - + meetings

@@ -40,7 +40,7 @@ const Hero = () => {
diff --git a/src/components/room/chat/chat-container.tsx b/src/components/room/chat/chat-container.tsx index fcf1262..8c36d64 100644 --- a/src/components/room/chat/chat-container.tsx +++ b/src/components/room/chat/chat-container.tsx @@ -33,7 +33,10 @@ const ChatContainer = () => { -
+
{ return (
+ {/* change padding to pt-10 */} {/* screen sharing */} {/* peers grid */} diff --git a/src/components/room/emoji.tsx b/src/components/room/emoji.tsx index f00a558..4ac9697 100644 --- a/src/components/room/emoji.tsx +++ b/src/components/room/emoji.tsx @@ -7,8 +7,9 @@ const Emoji = () => { variant="ghost" size="icon" className="w-12 h-12 rounded-xl text-white - bg-gradient-to-bl from-white/15 to-white/1 backdrop-blur-xl + bg-linear-to-bl from-white/15 to-white/1 backdrop-blur-xl " + title="Coming soon" > diff --git a/src/components/room/end.tsx b/src/components/room/end.tsx index 946c57b..6a7fe2a 100644 --- a/src/components/room/end.tsx +++ b/src/components/room/end.tsx @@ -3,7 +3,7 @@ import { MoreVertical, PhoneOff } from 'lucide-react'; const End = () => { const handleLeaveCall = () => { - console.log('Leaving call...'); + window.location.reload(); }; return (
diff --git a/src/components/room/grid/my-tile.tsx b/src/components/room/grid/my-tile.tsx index d39bd29..cfda21b 100644 --- a/src/components/room/grid/my-tile.tsx +++ b/src/components/room/grid/my-tile.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef } from 'react'; import { Mic, MicOff } from 'lucide-react'; import type { Layout } from '@/types'; -import { getInitials } from '@/lib/utils'; +import { cn, getInitials } from '@/lib/utils'; import { useCameraDeviceId, useCameraOn, @@ -56,7 +56,14 @@ const MyTile: React.FC = ({ layout }) => { )} {/* Mic Status */} -
+
{micOn ? ( ) : ( diff --git a/src/components/room/grid/peer-tile.tsx b/src/components/room/grid/peer-tile.tsx index 0b26cf7..face2a6 100644 --- a/src/components/room/grid/peer-tile.tsx +++ b/src/components/room/grid/peer-tile.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef } from 'react'; import { Mic, MicOff } from 'lucide-react'; import type { Layout } from '@/types'; -import { getInitials } from '@/lib/utils'; +import { cn, getInitials } from '@/lib/utils'; import { usePeerMediasById, usePeerOthersById } from '@/store/conf/hooks'; import { useMedia } from '@/hooks/use-media'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; @@ -48,14 +48,21 @@ export const PeerTile: React.FC = ({ peerId, layout }) => { ) : ( {/* */} - + {getInitials(peerData.name)} )} {/* Mic Status */} -
+
{media?.mic ? ( ) : ( diff --git a/src/components/room/hand.tsx b/src/components/room/hand.tsx index d32a20b..fc6f8cb 100644 --- a/src/components/room/hand.tsx +++ b/src/components/room/hand.tsx @@ -12,11 +12,12 @@ const Hand = () => { variant="ghost" size="icon" className={cn( - 'w-12 h-12 rounded-xl transition-all duration-200 bg-gradient-to-bl text-white', + 'w-12 h-12 rounded-xl transition-all duration-200 bg-linear-to-bl text-white', isHandRaised - ? 'bg-yellow-600 hover:bg-yellow-700 ' + ? 'from-white/10 hover:from-white/15 ' : ' from-white/15 to-white/1 backdrop-blur-xl' )} + title="Coming soon" > diff --git a/src/components/room/header.tsx b/src/components/room/header.tsx index 1292091..75703b9 100644 --- a/src/components/room/header.tsx +++ b/src/components/room/header.tsx @@ -1,25 +1,13 @@ -import { Loader2 } from 'lucide-react'; -import { Badge } from '../ui/badge'; - const Header = () => { return ( -
- {/* Left side - Meeting info */} -
-
- {/*
*/} - - {/*
*/} - {/* Mitsi */} -
-
+
+ {/* Logo */} - {/* Right side - Recording indicator */}
- + {/*
REC - + */}
); diff --git a/src/components/room/join/join-camera-preview.tsx b/src/components/room/join/join-camera-preview.tsx index f85701d..5f5dc6c 100644 --- a/src/components/room/join/join-camera-preview.tsx +++ b/src/components/room/join/join-camera-preview.tsx @@ -35,18 +35,9 @@ const CameraPreview = () => { /> ) : (
- {/* Enhanced avatar container with animated background */}
- {/* Animated glow rings */} -
-
- - {/* Main avatar */} -
+
- - {/* Floating particles around avatar */} - {/*
*/}
@@ -74,11 +65,6 @@ const CameraPreview = () => {
- - {/* Floating elements around video container */} - - {/*
*/} - {/*
*/}
); }; diff --git a/src/components/room/menu.tsx b/src/components/room/menu.tsx index 82e929d..526dae1 100644 --- a/src/components/room/menu.tsx +++ b/src/components/room/menu.tsx @@ -7,8 +7,9 @@ const Menu = () => { variant="ghost" size="icon" className="w-12 h-12 rounded-xl text-white - bg-gradient-to-tl from-white/15 to-white/1 backdrop-blur-xl + bg-linear-to-tl from-white/15 to-white/1 backdrop-blur-xl " + title="Coming soon" > diff --git a/src/components/room/sidebar.tsx b/src/components/room/sidebar.tsx index e809cbe..1b1a297 100644 --- a/src/components/room/sidebar.tsx +++ b/src/components/room/sidebar.tsx @@ -25,7 +25,7 @@ const Sidebar = () => { return (
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..a1b647f --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,38 @@ +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { Toaster as Sonner, type ToasterProps } from 'sonner'; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + '--normal-bg': 'var(--popover)', + '--normal-text': 'var(--popover-foreground)', + '--normal-border': 'var(--border)', + '--border-radius': 'var(--radius)', + } as React.CSSProperties + } + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/src/hooks/use-media.ts b/src/hooks/use-media.ts index 70585d6..3b03469 100644 --- a/src/hooks/use-media.ts +++ b/src/hooks/use-media.ts @@ -163,6 +163,16 @@ export const useMedia = () => { }); }, [mediaService, signalingService]); + const produceUserMedia = useCallback(async () => { + if (roomAccess != Access.Allowed) return; + if (micOn) await createProducer('mic'); + if (cameraOn) await createProducer('camera'); + if (screenOn) { + await createProducer('screen'); + await createProducer('screenAudio'); + } + }, [roomAccess, micOn, cameraOn, screenOn, createProducer]); + const closeAllProducers = useCallback(() => { if (!mediaService) throw new Error('MediaService not initialized'); mediaService.closeAllProducers(); @@ -425,6 +435,7 @@ export const useMedia = () => { resumeConsumer, closeConsumer, createWebRtcConnections, + produceUserMedia, closeAllProducers, closeAllConsumers, closeAllTransports, diff --git a/src/providers/room-provider.tsx b/src/providers/room-provider.tsx index 48d6d4b..61eb524 100644 --- a/src/providers/room-provider.tsx +++ b/src/providers/room-provider.tsx @@ -2,26 +2,28 @@ import { useMedia } from '@/hooks/use-media'; import { useRoom } from '@/hooks/use-room'; import { useSignaling } from '@/hooks/use-signaling'; import { HEARTBEAT_INTERVAL } from '@/lib/constants'; -import { useToastActions } from '@/packages/toast'; import { useRoomAccess, useRoomActions } from '@/store/conf/hooks'; import { Access, type AckCallbackData, type MessageData } from '@/types'; import { Actions } from '@/types/actions'; import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { toast } from 'sonner'; const RoomProvider = ({ children }: { children: ReactNode }) => { const { signalingService, sendHeartBeat } = useSignaling(); - const toastActions = useToastActions(); const { mediaService, createWebRtcConnections, closeAllConsumers, closeAllTransports, closeAllProducers, + produceUserMedia, } = useMedia(); + const reconnectionToastRef = useRef(0); + const heartBeatIntervalRef = useRef(null); const roomAccess = useRoomAccess(); const roomActions = useRoomActions(); - const [rejoining, setrejoining] = useState(false); + const [rejoining, setRejoining] = useState(false); const { joinRoom, leaveRoom, actionHandlers } = useRoom(); useEffect(() => { @@ -54,6 +56,7 @@ const RoomProvider = ({ children }: { children: ReactNode }) => { (async () => { await joinRoom(); await createWebRtcConnections(); + await produceUserMedia(); // register heartbeat interval heartBeatIntervalRef.current = setInterval( sendHeartBeat, @@ -67,20 +70,35 @@ const RoomProvider = ({ children }: { children: ReactNode }) => { if (heartBeatIntervalRef.current) clearInterval(heartBeatIntervalRef.current); }; - }, [roomAccess, createWebRtcConnections, joinRoom, sendHeartBeat, leaveRoom]); + }, [ + roomAccess, + createWebRtcConnections, + produceUserMedia, + joinRoom, + sendHeartBeat, + leaveRoom, + ]); useEffect(() => { if (roomAccess !== Access.Allowed) return; if (!rejoining) return; (async () => { - closeAllProducers(); + // closeAllProducers(); closeAllConsumers(); closeAllTransports(); await joinRoom(rejoining); await createWebRtcConnections(); + await produceUserMedia(); + setRejoining(false); + + if (reconnectionToastRef.current) + toast.dismiss(reconnectionToastRef.current); - setrejoining(false); + toast.success('You are reconnected', { + closeButton: true, + richColors: true, + }); })().catch(err => console.log(err)); }, [ rejoining, @@ -91,8 +109,15 @@ const RoomProvider = ({ children }: { children: ReactNode }) => { closeAllConsumers, closeAllTransports, closeAllProducers, + produceUserMedia, ]); + // produce on join + useEffect(() => { + if (roomAccess !== Access.Allowed) return; + if (rejoining) return; + }, [roomAccess, rejoining]); + useEffect(() => { if (!signalingService) return; if (roomAccess !== Access.Allowed) return; @@ -100,22 +125,24 @@ const RoomProvider = ({ children }: { children: ReactNode }) => { const connection = signalingService.getConnection(); // TODO - NEXT LINE OF ACTIONS connection.on('disconnect', () => { - console.log('Disconnected from signaling server'); if (connection.active) { + reconnectionToastRef.current = toast.loading( + 'You are disconnected, attempting to reconnect', + { + position: 'top-center', + richColors: true, + } + ); // Attempt to reconnect - toastActions.error('Reconnecting'); - console.log('Attempting to reconnect to signaling server...'); roomActions.setReconnecting(true); } else { - console.log('Connection is not active. Will not attempt to reconnect.'); roomActions.setDisconnected(true); } }); connection.io.on('reconnect', () => { - console.log('Reconnected to signaling server'); roomActions.setReconnecting(false); - setrejoining(true); + setRejoining(true); }); return () => { // Cleanup on unmount diff --git a/src/services/media-service.ts b/src/services/media-service.ts index 31aa5d9..de9d2cd 100644 --- a/src/services/media-service.ts +++ b/src/services/media-service.ts @@ -224,11 +224,13 @@ class MediaService { throw new Error('Send transport not initialized'); } let producer: mediasoupTypes.Producer; - const clonedTrack = this.getTrack(source); + const track = this.getTrack(source); - if (!clonedTrack) + if (!track) throw `${source} track was not found -- start ${source} before you create producer`; + const clonedTrack = track.clone(); + if (source === 'camera') { // Simulcast encoding settings const encodings = [