diff --git a/app/(browse)/[username]/page.tsx b/app/(browse)/[username]/page.tsx index 500acdf..8399b59 100644 --- a/app/(browse)/[username]/page.tsx +++ b/app/(browse)/[username]/page.tsx @@ -3,8 +3,7 @@ import { notFound } from "next/navigation"; import { getUserByUsername } from "@/lib/user-service"; import { isFollowingUser } from "@/lib/follow-service"; import { isBlockedByUser } from "@/lib/block-service"; -// import StreamPlayer from "@/components/stream-player"; -import Actions from "./_components/actions"; +import StreamPlayer from "@/components/stream-player"; interface UserPageProps { params: { @@ -27,23 +26,11 @@ const UserPage = async ({ params }: UserPageProps) => { } return ( - // - - // TODO: Remove this -
-

Username: {user.username}

-

user ID: {user.id}

-

is following: {`${isFollowing}`}

-

is blocked: {`${isBlocked}`}

- -
+ ); }; diff --git a/app/(browse)/_components/navbar/actions.tsx b/app/(browse)/_components/navbar/actions.tsx index 8e0dd70..5849ec0 100644 --- a/app/(browse)/_components/navbar/actions.tsx +++ b/app/(browse)/_components/navbar/actions.tsx @@ -24,7 +24,7 @@ const Actions = async () => { > - Dashboard + Dashboard diff --git a/app/(browse)/_components/sidebar/toggle.tsx b/app/(browse)/_components/sidebar/toggle.tsx index f7ec4a1..9465886 100644 --- a/app/(browse)/_components/sidebar/toggle.tsx +++ b/app/(browse)/_components/sidebar/toggle.tsx @@ -33,7 +33,9 @@ const Toggle = () => { )} {!collapsed && (
-

Made for you

+

+ Made for you +

{ )} {!collapsed && (
-

Dashboard

+

Dashboard

void; +} + +const FullscreenControl = ({ + isFullscreen, + onToggle, +}: FullscreenControlProps) => { + const Icon = isFullscreen ? Minimize : Maximize; + + const label = isFullscreen ? "Exit fullscreen" : "Enter fullscreen"; + + return ( +
+ + + +
+ ); +}; + +export default FullscreenControl; diff --git a/components/stream-player/index.tsx b/components/stream-player/index.tsx index 1896d8f..678904d 100644 --- a/components/stream-player/index.tsx +++ b/components/stream-player/index.tsx @@ -4,14 +4,14 @@ import { Stream, User } from "@prisma/client"; import { LiveKitRoom } from "@livekit/components-react"; import { cn } from "@/lib/utils"; -// import { useChatSidebar } from "@/store/use-chat-sidebar"; +import { useChatSidebar } from "@/store/use-chat-sidebar"; import { useViewerToken } from "@/hooks/use-viewer-token"; // import { InfoCard } from "./info-card"; // import { AboutCard } from "./about-card"; // import { ChatToggle } from "./chat-toggle"; // import { Chat, ChatSkeleton } from "./chat"; -// import { Video, VideoSkeleton } from "./video"; +import Video, { VideoSkeleton } from "./video"; // import { Header, HeaderSkeleton } from "./header"; type CustomStream = { @@ -41,70 +41,67 @@ interface StreamPlayerProps { const StreamPlayer = ({ user, stream, isFollowing }: StreamPlayerProps) => { const { token, name, identity } = useViewerToken(user.id); - // const { collapsed } = useChatSidebar((state) => state); + const { collapsed } = useChatSidebar((state) => state); if (!token || !name || !identity) { return ; } return ( - // <> - // {collapsed && ( - //
- // - //
- // )} - // - //
- //
- //
- // - //
- //
- // - - // TODO: Remove this - <>STREAM PLAYER + <> + {/* {collapsed && ( +
+ +
+ )} */} + +
+
+ {/*
+ +
*/} +
+ ); }; @@ -114,7 +111,7 @@ export const StreamPlayerSkeleton = () => { return (
- {/* */} + {/* */}
{/* */}
diff --git a/components/stream-player/live-video.tsx b/components/stream-player/live-video.tsx new file mode 100644 index 0000000..555c54c --- /dev/null +++ b/components/stream-player/live-video.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useRef, useState, useEffect } from "react"; +import { Participant, Track } from "livekit-client"; +import { useTracks } from "@livekit/components-react"; +import { useEventListener } from "usehooks-ts"; + +import VolumeControl from "./volume-control"; +import FullscreenControl from "./fullscreen-control"; + +interface LiveVideoProps { + participant: Participant; +} + +const LiveVideo = ({ participant }: LiveVideoProps) => { + const videoRef = useRef(null); + const wrapperRef = useRef(null); + + const [isFullscreen, setIsFullscreen] = useState(false); + const [volume, setVolume] = useState(0); + + const onVolumeChange = (value: number) => { + setVolume(+value); + if (videoRef?.current) { + videoRef.current.muted = value === 0; + videoRef.current.volume = +value * 0.01; + } + }; + + const toggleMute = () => { + const isMuted = volume === 0; + + setVolume(isMuted ? 50 : 0); + + if (videoRef?.current) { + videoRef.current.muted = !isMuted; + videoRef.current.volume = isMuted ? 0.5 : 0; + } + }; + + useEffect(() => { + onVolumeChange(0); + }, []); + + const toggleFullscreen = () => { + if (isFullscreen) { + document.exitFullscreen(); + } else if (wrapperRef?.current) { + wrapperRef.current.requestFullscreen(); + } + }; + + const handleFullscreenChange = () => { + const isCurrentlyFullscreen = document.fullscreenElement !== null; + setIsFullscreen(isCurrentlyFullscreen); + }; + + useEventListener("fullscreenchange", handleFullscreenChange, wrapperRef); + + useTracks([Track.Source.Camera, Track.Source.Microphone]) + .filter((track) => track.participant.identity === participant.identity) + .forEach((track) => { + if (videoRef.current) { + track.publication.track?.attach(videoRef.current); + } + }); + + return ( +
+
+ ); +}; + +export default LiveVideo; diff --git a/components/stream-player/loading-video.tsx b/components/stream-player/loading-video.tsx new file mode 100644 index 0000000..813372a --- /dev/null +++ b/components/stream-player/loading-video.tsx @@ -0,0 +1,16 @@ +import { Loader } from "lucide-react"; + +interface LoadingVideoProps { + label: string; +} + +const LoadingVideo = ({ label }: LoadingVideoProps) => { + return ( +
+ +

{label}

+
+ ); +}; + +export default LoadingVideo; diff --git a/components/stream-player/offline-video.tsx b/components/stream-player/offline-video.tsx new file mode 100644 index 0000000..a011114 --- /dev/null +++ b/components/stream-player/offline-video.tsx @@ -0,0 +1,16 @@ +import { WifiOff } from "lucide-react"; + +interface OfflineVideoProps { + username: string; +} + +const OfflineVideo = ({ username }: OfflineVideoProps) => { + return ( +
+ +

{username} is offline

+
+ ); +}; + +export default OfflineVideo; diff --git a/components/stream-player/video.tsx b/components/stream-player/video.tsx new file mode 100644 index 0000000..1ee8ba2 --- /dev/null +++ b/components/stream-player/video.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { ConnectionState, Track } from "livekit-client"; +import { + useConnectionState, + useRemoteParticipant, + useTracks, +} from "@livekit/components-react"; + +import { Skeleton } from "@/components/ui/skeleton"; + +import OfflineVideo from "./offline-video"; +import LoadingVideo from "./loading-video"; +import LiveVideo from "./live-video"; + +interface VideoProps { + hostName: string; + hostIdentity: string; +} + +const Video = ({ hostName, hostIdentity }: VideoProps) => { + const connectionState = useConnectionState(); + const participant = useRemoteParticipant(hostIdentity); + const tracks = useTracks([ + Track.Source.Camera, + Track.Source.Microphone, + ]).filter((track) => track.participant.identity === hostIdentity); + + let content; + + if (!participant && connectionState === ConnectionState.Connected) { + content = ; + } else if (!participant || tracks.length === 0) { + content = ; + } else { + content = ; + } + //TODO: check border if I remove it or not + return
{content}
; +}; + +export default Video; + +export const VideoSkeleton = () => { + return ( +
+ +
+ ); +}; diff --git a/components/stream-player/volume-control.tsx b/components/stream-player/volume-control.tsx new file mode 100644 index 0000000..7121998 --- /dev/null +++ b/components/stream-player/volume-control.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Volume1, Volume2, VolumeX } from "lucide-react"; + +import Hint from "@/components/hint"; +import { Slider } from "@/components/ui/slider"; +import { Button } from "../ui/button"; + +interface VolumeControlProps { + onToggle: () => void; + onChange: (value: number) => void; + value: number; +} + +const VolumeControl = ({ onToggle, onChange, value }: VolumeControlProps) => { + const isMuted = value === 0; + const isAboveHalf = value > 50; + + let Icon = Volume1; + + if (isMuted) { + Icon = VolumeX; + } else if (isAboveHalf) { + Icon = Volume2; + } + + const label = isMuted ? "Unmute" : "Mute"; + + const handleChange = (value: number[]) => { + onChange(value[0]); + }; + + return ( +
+ + + + +
+ ); +}; + +export default VolumeControl; diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx new file mode 100644 index 0000000..c31c2b3 --- /dev/null +++ b/components/ui/slider.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/next.config.js b/next.config.js index 3d06ef0..7278a95 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,15 @@ const nextConfig = { }, ], }, + // webpack: (config) => { + // config.module.rules.push({ + // test: /\.mjs$/, + // include: /node_modules/, + // type: "javascript/auto", + // }); + + // return config; + // }, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index e1f8f39..4fa258f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", @@ -1192,6 +1193,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", + "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", diff --git a/package.json b/package.json index 400c6b0..3a4d991 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/store/use-chat-sidebar.ts b/store/use-chat-sidebar.ts new file mode 100644 index 0000000..f06752b --- /dev/null +++ b/store/use-chat-sidebar.ts @@ -0,0 +1,22 @@ +import { create } from "zustand"; + +export enum ChatVariant { + CHAT = "CHAT", + COMMUNITY = "COMMUNITY", +} + +interface ChatSidebarStore { + collapsed: boolean; + variant: ChatVariant; + onExpand: () => void; + onCollapse: () => void; + onChangeVariant: (variant: ChatVariant) => void; +} + +export const useChatSidebar = create((set) => ({ + collapsed: false, + variant: ChatVariant.CHAT, + onExpand: () => set(() => ({ collapsed: false })), + onCollapse: () => set(() => ({ collapsed: true })), + onChangeVariant: (variant: ChatVariant) => set(() => ({ variant })), +}));