From 21a05dd202c717bca0dd835a725f91e479b60c92 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Thu, 15 May 2025 05:20:01 +0300 Subject: [PATCH] . --- app/app.css | 141 +++++- app/components/app-layout.tsx | 122 +++++ app/components/channel-list-item.tsx | 45 ++ app/components/create-server.tsx | 113 +++++ app/components/global-webrtc-audio-player.tsx | 51 ++ app/components/icons/Discord.tsx | 5 + .../gateway-websocket-connection-manager.tsx | 47 ++ .../manager/webrtc-connection-manager.tsx | 185 ++++++++ app/components/online-status.tsx | 20 + app/components/password-input.tsx | 40 ++ app/components/private-channel-list-item.tsx | 43 ++ app/components/text-box.tsx | 125 +++++ app/components/theme/theme-provider.tsx | 73 +++ app/components/theme/theme-toggle.tsx | 37 ++ app/components/ui/avatar.tsx | 52 +++ app/components/ui/badge.tsx | 46 ++ app/components/ui/button.tsx | 60 +++ app/components/ui/card.tsx | 87 ++++ app/components/ui/dialog.tsx | 134 ++++++ app/components/ui/dropdown-menu.tsx | 247 ++++++++++ app/components/ui/form.tsx | 165 +++++++ app/components/ui/icon-upload-field.tsx | 121 +++++ app/components/ui/input.tsx | 21 + app/components/ui/label.tsx | 24 + app/components/ui/scroll-area.tsx | 79 ++++ app/components/ui/separator.tsx | 26 ++ app/components/ui/tabs.tsx | 64 +++ app/components/ui/textarea.tsx | 18 + app/components/ui/tooltip.tsx | 59 +++ app/lib/api/client/auth.ts | 34 ++ app/lib/api/client/server.ts | 24 + app/lib/api/client/test.ts | 14 + app/lib/api/client/user.ts | 19 + app/lib/api/http-client.ts | 23 + app/lib/api/types.ts | 36 ++ app/lib/consts.ts | 1 + app/lib/utils.ts | 14 + app/lib/websocket/gateway.types.ts | 79 ++++ app/lib/websocket/voice.types.ts | 33 ++ app/root.tsx | 14 +- app/routes.ts | 27 +- app/routes/app/index.tsx | 58 +++ app/routes/app/layout.tsx | 43 ++ app/routes/app/me/channel.tsx | 69 +++ app/routes/app/me/index.tsx | 23 + app/routes/app/me/layout.tsx | 49 ++ app/routes/app/server/channel.tsx | 23 + app/routes/app/server/index.tsx | 23 + app/routes/app/server/layout.tsx | 53 +++ app/routes/auth/layout.tsx | 11 + app/routes/auth/login.tsx | 114 +++++ app/routes/auth/register.tsx | 116 +++++ app/routes/home.tsx | 13 - app/routes/index.tsx | 16 + app/store/active-voice-channel.ts | 39 ++ app/store/gateway-websocket.ts | 435 ++++++++++++++++++ app/store/private-channels.ts | 14 + app/store/server-channels-list.ts | 20 + app/store/server-list.ts | 22 + app/store/token.ts | 21 + app/store/user.ts | 14 + app/store/voice-websocket.ts | 408 ++++++++++++++++ app/store/webrtc.ts | 332 +++++++++++++ app/welcome/logo-dark.svg | 23 - app/welcome/logo-light.svg | 23 - app/welcome/welcome.tsx | 89 ---- bun.lock | 160 +++++++ components.json | 21 + package.json | 22 +- react-router.config.ts | 2 +- 70 files changed, 4663 insertions(+), 161 deletions(-) create mode 100644 app/components/app-layout.tsx create mode 100644 app/components/channel-list-item.tsx create mode 100644 app/components/create-server.tsx create mode 100644 app/components/global-webrtc-audio-player.tsx create mode 100644 app/components/icons/Discord.tsx create mode 100644 app/components/manager/gateway-websocket-connection-manager.tsx create mode 100644 app/components/manager/webrtc-connection-manager.tsx create mode 100644 app/components/online-status.tsx create mode 100644 app/components/password-input.tsx create mode 100644 app/components/private-channel-list-item.tsx create mode 100644 app/components/text-box.tsx create mode 100644 app/components/theme/theme-provider.tsx create mode 100644 app/components/theme/theme-toggle.tsx create mode 100644 app/components/ui/avatar.tsx create mode 100644 app/components/ui/badge.tsx create mode 100644 app/components/ui/button.tsx create mode 100644 app/components/ui/card.tsx create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/dropdown-menu.tsx create mode 100644 app/components/ui/form.tsx create mode 100644 app/components/ui/icon-upload-field.tsx create mode 100644 app/components/ui/input.tsx create mode 100644 app/components/ui/label.tsx create mode 100644 app/components/ui/scroll-area.tsx create mode 100644 app/components/ui/separator.tsx create mode 100644 app/components/ui/tabs.tsx create mode 100644 app/components/ui/textarea.tsx create mode 100644 app/components/ui/tooltip.tsx create mode 100644 app/lib/api/client/auth.ts create mode 100644 app/lib/api/client/server.ts create mode 100644 app/lib/api/client/test.ts create mode 100644 app/lib/api/client/user.ts create mode 100644 app/lib/api/http-client.ts create mode 100644 app/lib/api/types.ts create mode 100644 app/lib/consts.ts create mode 100644 app/lib/utils.ts create mode 100644 app/lib/websocket/gateway.types.ts create mode 100644 app/lib/websocket/voice.types.ts create mode 100644 app/routes/app/index.tsx create mode 100644 app/routes/app/layout.tsx create mode 100644 app/routes/app/me/channel.tsx create mode 100644 app/routes/app/me/index.tsx create mode 100644 app/routes/app/me/layout.tsx create mode 100644 app/routes/app/server/channel.tsx create mode 100644 app/routes/app/server/index.tsx create mode 100644 app/routes/app/server/layout.tsx create mode 100644 app/routes/auth/layout.tsx create mode 100644 app/routes/auth/login.tsx create mode 100644 app/routes/auth/register.tsx delete mode 100644 app/routes/home.tsx create mode 100644 app/routes/index.tsx create mode 100644 app/store/active-voice-channel.ts create mode 100644 app/store/gateway-websocket.ts create mode 100644 app/store/private-channels.ts create mode 100644 app/store/server-channels-list.ts create mode 100644 app/store/server-list.ts create mode 100644 app/store/token.ts create mode 100644 app/store/user.ts create mode 100644 app/store/voice-websocket.ts create mode 100644 app/store/webrtc.ts delete mode 100644 app/welcome/logo-dark.svg delete mode 100644 app/welcome/logo-light.svg delete mode 100644 app/welcome/welcome.tsx create mode 100644 components.json diff --git a/app/app.css b/app/app.css index 99345d8..9e56f42 100644 --- a/app/app.css +++ b/app/app.css @@ -1,15 +1,146 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); @theme { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } -html, -body { - @apply bg-white dark:bg-gray-950; +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} - @media (prefers-color-scheme: dark) { - color-scheme: dark; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + } + + button, + [role="button"] { + cursor: pointer; } } + +@layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + /* IE and Edge */ + -ms-overflow-style: none; + /* Firefox */ + scrollbar-width: none; + } +} \ No newline at end of file diff --git a/app/components/app-layout.tsx b/app/components/app-layout.tsx new file mode 100644 index 0000000..35d15a6 --- /dev/null +++ b/app/components/app-layout.tsx @@ -0,0 +1,122 @@ +import { Headphones, Mic, Settings } from "lucide-react"; +import React from "react"; +import { NavLink } from "react-router"; +import { useShallow } from 'zustand/react/shallow'; +import { cn, getFirstLetters } from "~/lib/utils"; +import { useServerListStore } from "~/store/server-list"; +import { useUserStore } from "~/store/user"; +import { CreateServerButton } from "./create-server"; +import Discord from "./icons/Discord"; +import { OnlineStatus } from "./online-status"; +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; +import { Button } from "./ui/button"; +import { ScrollArea } from "./ui/scroll-area"; +import { Separator } from "./ui/separator"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; + +interface AppLayoutProps { + list: React.ReactNode; + children: React.ReactNode; +} + +export default function AppLayout({ list, children }: AppLayoutProps) { + let user = useUserStore(state => state.user!) + let servers = useServerListStore(useShallow((state) => Array.from(state.servers.values()))) + + return ( +
+
+
+ + + +
+
+ {list} +
+ +
+
+
+
+ + + + + + +
+
+ {user.displayName || user.username} +
+ @{user.username} +
+
+
+
+ + + +
+
+
+
+
+
+ +
+ {children} +
+
+ ) +} \ No newline at end of file diff --git a/app/components/channel-list-item.tsx b/app/components/channel-list-item.tsx new file mode 100644 index 0000000..a2f5f58 --- /dev/null +++ b/app/components/channel-list-item.tsx @@ -0,0 +1,45 @@ +import { ChevronDown, Hash, Volume2 } from "lucide-react" +import { Button } from "./ui/button" + +interface Channel { + id: string + name: string + type: "text" | "voice" | "category" +} + +interface ChannelListItemProps { + channel: Channel +} + +export default function ChannelListItem({ channel }: ChannelListItemProps) { + if (channel.type === "category") { + return ( +
+
+
+ {channel.name} + +
+
+
+
+
+ ) + } + + return ( + <> + + + ) +} diff --git a/app/components/create-server.tsx b/app/components/create-server.tsx new file mode 100644 index 0000000..fdd5375 --- /dev/null +++ b/app/components/create-server.tsx @@ -0,0 +1,113 @@ +import { CirclePlus } from "lucide-react"; +import React from "react"; + + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import server from "~/lib/api/client/server"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; +import { IconUploadField } from "./ui/icon-upload-field"; +import { Input } from "./ui/input"; + +const schema = z.object({ + name: z.string().min(1).max(32), + icon: z.instanceof(File).optional(), +}); + +export function CreateServerButton() { + const [open, setOpen] = React.useState(false); + + let form = useForm>({ + resolver: zodResolver(schema), + }) + + function onOpenChange(openState: boolean) { + setOpen(openState) + + if (!openState) { + form.reset() + } + } + + async function onSubmit(values: z.infer) { + const response = await server.create(values) + + onOpenChange(false) + } + + return ( + + + + + + + Create server + + Give your server a name and choose a server icon. + + + +
+ + ( + + Icon + +
+ +
+
+ +
+ )} + /> + ( + + Name + + + + + + )} + /> + + + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/app/components/global-webrtc-audio-player.tsx b/app/components/global-webrtc-audio-player.tsx new file mode 100644 index 0000000..73ea373 --- /dev/null +++ b/app/components/global-webrtc-audio-player.tsx @@ -0,0 +1,51 @@ +// ~/components/global-audio-player.tsx +import { useEffect, useRef } from 'react'; +import { useWebRTCStore } from '~/store/webrtc'; + +export function GlobalWebRTCAudioPlayer() { + const audioRef = useRef(null); + const remoteStream = useWebRTCStore(state => state.remoteStream); + const webRTCStatus = useWebRTCStore(state => state.status); + + useEffect(() => { + const audioElement = audioRef.current; + if (audioElement) { + if (remoteStream && webRTCStatus === 'CONNECTED') { + console.log('GlobalAudioPlayer: Setting remote stream to audio element.'); + if (audioElement.srcObject !== remoteStream) { // Avoid unnecessary re-assignments + audioElement.srcObject = remoteStream; + audioElement.play().catch(error => { + // Autoplay policy might prevent play without user interaction. + // You might need a UI element for the user to click to start playback. + console.warn('GlobalAudioPlayer: Error trying to play audio automatically:', error); + // A common pattern is to mute the element initially and then allow unmuting by user action + // audioElement.muted = true; // Then provide an unmute button + }); + } + } else { + // If stream is null or not connected, clear the srcObject + // This also handles the case when leaving a channel. + if (audioElement.srcObject) { + console.log('GlobalAudioPlayer: Clearing remote stream from audio element.'); + audioElement.srcObject = null; + } + } + } + }, [remoteStream, webRTCStatus]); // Re-run when remoteStream or connection status changes + + // This component renders the audio element. + // It's generally good practice for