From f6f3892e3bfa7e81a2d8e40ff8465a319147eba6 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 25 Feb 2025 10:28:16 +0100 Subject: [PATCH 1/4] feat: make it work on mobile --- browser-chat/frontend/package-lock.json | 40 ++++ browser-chat/frontend/package.json | 2 + browser-chat/frontend/src/app.tsx | 22 +-- .../src/components/change-nickname-button.tsx | 30 +-- .../frontend/src/components/chatview.tsx | 180 +++++++++++------- .../frontend/src/components/header.tsx | 10 +- .../frontend/src/components/invitepopup.tsx | 178 +++++++++-------- .../src/components/leave-channel-button.tsx | 46 ++--- .../frontend/src/components/logview.tsx | 66 +++++-- .../src/components/ui/adaptive-dialog.tsx | 104 ++++++++++ .../frontend/src/components/ui/drawer.tsx | 116 +++++++++++ .../frontend/src/components/ui/toggle.tsx | 43 +++++ .../frontend/src/hooks/use-media-query.ts | 24 +++ browser-chat/frontend/src/lib/log.ts | 3 + browser-chat/frontend/src/lib/mock.ts | 170 +++++++++++++++++ 15 files changed, 814 insertions(+), 220 deletions(-) create mode 100644 browser-chat/frontend/src/components/ui/adaptive-dialog.tsx create mode 100644 browser-chat/frontend/src/components/ui/drawer.tsx create mode 100644 browser-chat/frontend/src/components/ui/toggle.tsx create mode 100644 browser-chat/frontend/src/hooks/use-media-query.ts create mode 100644 browser-chat/frontend/src/lib/mock.ts diff --git a/browser-chat/frontend/package-lock.json b/browser-chat/frontend/package-lock.json index d4e76317..d1e76d6d 100644 --- a/browser-chat/frontend/package-lock.json +++ b/browser-chat/frontend/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-toggle": "^1.1.2", "@types/react-timeago": "^4.1.7", "autoprefixer": "^10.4.20", "chat-browser": "file:../browser-wasm/pkg", @@ -26,6 +27,7 @@ "react-timeago": "^7.2.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", "yet-another-name-generator": "^1.2.0" }, "devDependencies": { @@ -1375,6 +1377,31 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -4448,6 +4475,19 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vite": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", diff --git a/browser-chat/frontend/package.json b/browser-chat/frontend/package.json index de07dbb8..b28a10e6 100644 --- a/browser-chat/frontend/package.json +++ b/browser-chat/frontend/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-toggle": "^1.1.2", "@types/react-timeago": "^4.1.7", "autoprefixer": "^10.4.20", "chat-browser": "file:../browser-wasm/pkg", @@ -30,6 +31,7 @@ "react-timeago": "^7.2.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", "yet-another-name-generator": "^1.2.0" }, "devDependencies": { diff --git a/browser-chat/frontend/src/app.tsx b/browser-chat/frontend/src/app.tsx index 5dcde44c..da720067 100644 --- a/browser-chat/frontend/src/app.tsx +++ b/browser-chat/frontend/src/app.tsx @@ -4,13 +4,12 @@ import { useState, useEffect } from "react" import HomeScreen from "./components/homescreen" import ChatView from "./components/chatview" import Header from "./components/header" -import LogView from "./components/logview" import Sidebar from "./components/sidebar" import { ThemeProvider } from "next-themes" -import { InvitePopup } from "./components/invitepopup" import { API, initApi, type ChannelInfo } from "./lib/api" import { generate as generateName } from 'yet-another-name-generator' import { log } from "./lib/log" +import { useIsDesktop } from "./hooks/use-media-query" export default function AppWrapper() { const [api, setApi] = useState(null) @@ -45,13 +44,9 @@ function Spinner() { } function SplashScreen({ children }: React.PropsWithChildren) { - const [showLogView, setShowLogView] = useState(false) return (
-
setShowLogView(!showLogView)} - /> - {showLogView && setShowLogView(false)} />} +
{children}
@@ -68,9 +63,7 @@ function App({ api }: AppProps) { const [currentView, setCurrentView] = useState<"home" | "chat">("home") const [channels, setChannels] = useState([]) const [activeChannel, setActiveChannel] = useState(null) - const [showLogView, setShowLogView] = useState(false) const [nickname, setNickname] = useState(generateName()) - const [showInvitePopup, setShowInvitePopup] = useState(false) const [showSidebar, setShowSidebar] = useState(false) const joinChannel = async (ticket: string) => { @@ -115,6 +108,8 @@ function App({ api }: AppProps) { setShowSidebar(true) } + const isDesktop = useIsDesktop() + let title if (activeChannel) { title = '#' + channels.find((c) => c.id === activeChannel)?.name @@ -122,7 +117,7 @@ function App({ api }: AppProps) { return ( <> - {(currentView === "chat" || showSidebar) && ( + {isDesktop && (currentView === "chat" || showSidebar) && (
setShowLogView(!showLogView)} title={title} - onInviteClick={activeChannel ? (() => setShowInvitePopup(true)) : undefined} /> {currentView === "home" && ( closeChannel(activeChannel)} /> )} - {showLogView && setShowLogView(false)} />} - {showInvitePopup && activeChannel && ( + {/* {showInvitePopup && activeChannel && ( { @@ -167,7 +159,7 @@ function App({ api }: AppProps) { channel={channels.find((c) => c.id === activeChannel)?.name || ""} getTicket={(opts) => api.getTicket(activeChannel!, opts)} /> - )} + )} */}
) diff --git a/browser-chat/frontend/src/components/change-nickname-button.tsx b/browser-chat/frontend/src/components/change-nickname-button.tsx index 32d9dd1c..d90f33d8 100644 --- a/browser-chat/frontend/src/components/change-nickname-button.tsx +++ b/browser-chat/frontend/src/components/change-nickname-button.tsx @@ -3,12 +3,12 @@ import { FormEvent, useEffect, useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" + AdaptiveDialog, + AdaptiveDialogContent, + AdaptiveDialogHeader, + AdaptiveDialogTitle, + AdaptiveDialogTrigger, +} from "@/components/ui/adaptive-dialog" import { API } from "@/lib/api" interface ChangeNicknameProps { @@ -33,19 +33,19 @@ export function ChangeNicknameButton({ api, channel }: ChangeNicknameProps) { } } return ( - - + + - - - - Change nickname - + + + + Change nickname +
setName(e.target.value)} placeholder="Enter your nickname" />
-
-
+ + ) } diff --git a/browser-chat/frontend/src/components/chatview.tsx b/browser-chat/frontend/src/components/chatview.tsx index 0123e00e..860371d4 100644 --- a/browser-chat/frontend/src/components/chatview.tsx +++ b/browser-chat/frontend/src/components/chatview.tsx @@ -8,24 +8,62 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { ArrowDown } from "lucide-react" +import { ArrowDown, ChevronLeft, Settings } from "lucide-react" import { type API, Message, PeerInfo, PeerRole } from "../lib/api" import { log } from "../lib/log" import clsx from "clsx" import { LeaveChannelButton } from "./leave-channel-button" import { ChangeNicknameButton } from "./change-nickname-button" -interface ChatViewProps { +import { useIsDesktop } from "@/hooks/use-media-query" +import { Toggle } from "./ui/toggle" +import { InviteButton } from "./invitepopup" + +interface ChatViewProps extends MessageViewProps { api: API channel: string onClose: () => void } export default function ChatView({ api, channel, onClose }: ChatViewProps) { + const isDesktop = useIsDesktop() + const [showMeta, setShowMeta] = useState(false) + const cls = clsx( + "flex flex-grow overflow-hidden", + ) + let extraButtons + if (!isDesktop) { + extraButtons = ( + + {!showMeta && } + {showMeta && } + + ) + } + return ( +
+ {(isDesktop || !showMeta) && ( + + )} + {(isDesktop) && ( +
+ +
+ )} + {(showMeta) && } +
+ ) +} + +interface MessageViewProps { + api: API + channel: string + extraButtons?: React.ReactElement +} + +export function MessageView({ api, channel, extraButtons }: MessageViewProps) { const [messages, setMessages] = useState([]) const [inputMessage, setInputMessage] = useState("") - const [peers, setPeers] = useState([]) - const [neighbors, setNeighbors] = useState(0) const [showScrollButton, setShowScrollButton] = useState(false) const [isScrolledToBottom, setIsScrolledToBottom] = useState(true) const messagesEndRef = useRef(null) @@ -40,14 +78,6 @@ export default function ChatView({ api, channel, onClose }: ChatViewProps) { setIsScrolledToBottom(true) }, []) - useEffect(() => { - return api.subscribeToNeighbors(channel, setNeighbors) - }, [channel]) - - useEffect(() => { - setPeers([...api.getPeers(channel)]) - return api.subscribeToPeers(channel, () => setPeers([...api.getPeers(channel)])) - }, [channel]) useEffect(() => { setMessages(api.getMessages(channel)) @@ -89,11 +119,6 @@ export default function ChatView({ api, channel, onClose }: ChatViewProps) { } } - const sortedPeers = [...peers].sort((a, b) => { - const statusOrder = { online: 0, away: 1, offline: 2 } - return statusOrder[a.status] - statusOrder[b.status] - }) - const handleScroll = useCallback(() => { if (scrollAreaRef.current) { const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef.current @@ -118,56 +143,81 @@ export default function ChatView({ api, channel, onClose }: ChatViewProps) { }, [handleScroll]) return ( -
-
- - {messages.map((msg) => ( -
- {msg.nickname || msg.sender.substring(0, 8)}: - {msg.content} -
- ))} -
- - {showScrollButton && ( - +
+ + {messages.map((msg) => ( +
+ {msg.nickname || msg.sender.substring(0, 8)}: + {msg.content} +
+ ))} +
+ + {showScrollButton && ( + + )} +
+ setInputMessage(e.target.value)} + placeholder="Type your message..." + className="flex-grow" + /> + + {extraButtons} +
+
+ ) +} + +function Meta({ api, channel, onClose, extraButtons }: ChatViewProps & { extraButtons?: React.ReactElement }) { + const [peers, setPeers] = useState([]) + const [neighbors, setNeighbors] = useState(0) + useEffect(() => { + return api.subscribeToNeighbors(channel, setNeighbors) + }, [channel]) + + useEffect(() => { + setPeers([...api.getPeers(channel)]) + return api.subscribeToPeers(channel, () => setPeers([...api.getPeers(channel)])) + }, [channel]) + const sortedPeers = [...peers].sort((a, b) => { + const statusOrder = { online: 0, away: 1, offline: 2 } + return statusOrder[a.status] - statusOrder[b.status] + }) + + return ( +
+
+ {extraButtons} +
+
+

Status

+ {neighbors > 0 && ( +

Connected ({neighbors} neighbors)

+ )} + {neighbors === 0 && ( +

Waiting for peers

)} -
- setInputMessage(e.target.value)} - placeholder="Type your message..." - className="flex-grow" - /> - -
-
-
-

Status

- {neighbors > 0 && ( -

Connected ({neighbors} neighbors)

- )} - {neighbors === 0 && ( -

Waiting for peers

- )} -
-
- - -
-

Peers

-
- - {sortedPeers.map((peer) => ( - - ))} - -
+
+ api.getTicket(channel, opts)} /> +
+
+ + +
+

Peers

+
+ + {sortedPeers.map((peer) => ( + + ))} +
) diff --git a/browser-chat/frontend/src/components/header.tsx b/browser-chat/frontend/src/components/header.tsx index 8fd3e35a..fb22acfd 100644 --- a/browser-chat/frontend/src/components/header.tsx +++ b/browser-chat/frontend/src/components/header.tsx @@ -1,18 +1,17 @@ "use client" import { Button } from "@/components/ui/button" -import { UserPlus, FileText, Moon, Sun } from "lucide-react" +import { UserPlus, Moon, Sun } from "lucide-react" import { useTheme } from "next-themes" import { useEffect, useState } from "react" +import { LogViewButton } from "./logview" interface HeaderProps { - onLogsClick: () => void onInviteClick?: () => void title?: string | null } export default function Header({ - onLogsClick: onLogsClick, onInviteClick, title, }: HeaderProps) { @@ -28,10 +27,11 @@ export default function Header({ Invite )} - + */}
diff --git a/browser-chat/frontend/src/components/invitepopup.tsx b/browser-chat/frontend/src/components/invitepopup.tsx index ca7b0bba..14e01905 100644 --- a/browser-chat/frontend/src/components/invitepopup.tsx +++ b/browser-chat/frontend/src/components/invitepopup.tsx @@ -5,12 +5,16 @@ import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { - Dialog, - DialogContent, - DialogTitle, -} from "@/components/ui/dialog" -import { Copy } from "lucide-react" + AdaptiveDialog, + AdaptiveDialogContent, + AdaptiveDialogHeader, + AdaptiveDialogTitle, + AdaptiveDialogTrigger, +} from "@/components/ui/adaptive-dialog" +import { Copy, Share2 } from "lucide-react" import { TicketOpts } from "@/lib/api" +import { useIsDesktop } from "@/hooks/use-media-query" +import clsx from "clsx" function ticketUrl(ticket: string) { const baseUrl = new URL(document.location.toString()) @@ -19,8 +23,6 @@ function ticketUrl(ticket: string) { } interface InvitePopupProps { - open: boolean - onOpenChange: (open: boolean) => void channel: string getTicket: (options: { includeMyself: boolean @@ -28,8 +30,37 @@ interface InvitePopupProps { includeNeighbors: boolean }) => string } +export function InviteButton({ channel, getTicket }: InvitePopupProps) { + const [open, setOpen] = useState(false) + const isDesktop = useIsDesktop() + const cls = clsx( + isDesktop ? 'w-2xl max-w-3xl' : 'max-w-[100vw]', + "max-h-[80vh]" + ) + return ( + + + + + +
+ + Invite peers + +
+ +
+
+
+
+ ) +} + -export function InvitePopup({ open, onOpenChange, channel, getTicket }: InvitePopupProps) { +export function InvitePopupContent({ channel, getTicket }: InvitePopupProps) { const [ticketOptions, setTicketOptions] = useState({ includeMyself: true, includeBootstrap: true, @@ -46,79 +77,72 @@ export function InvitePopup({ open, onOpenChange, channel, getTicket }: InvitePo const cliCommand = `cargo run -- join ${ticket}` return ( - - Invite to channel - -
-

Ticket

-
- {ticket.substring(0, 16)}... - -
-
- -
-

Join from the command line

- cliCommandRef.current?.select()} - /> -
-
-

Configure ticket

-
-
- setTicketOptions({ ...ticketOptions, includeMyself: !!checked })} - /> - -
-
- setTicketOptions({ ...ticketOptions, includeBootstrap: !!checked })} - /> - -
-
- setTicketOptions({ ...ticketOptions, includeNeighbors: !!checked })} - /> - -
+
+ +
+

Join from the command line

+ cliCommandRef.current?.select()} + /> + +
+
+

Configure ticket

+
+
+ setTicketOptions({ ...ticketOptions, includeMyself: !!checked })} + /> + +
+
+ setTicketOptions({ ...ticketOptions, includeBootstrap: !!checked })} + /> + +
+
+ setTicketOptions({ ...ticketOptions, includeNeighbors: !!checked })} + /> +
- {/* */} -
- -
- -
+
+ ) } diff --git a/browser-chat/frontend/src/components/leave-channel-button.tsx b/browser-chat/frontend/src/components/leave-channel-button.tsx index c2c1f634..2c0761a2 100644 --- a/browser-chat/frontend/src/components/leave-channel-button.tsx +++ b/browser-chat/frontend/src/components/leave-channel-button.tsx @@ -1,14 +1,11 @@ import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog" + AdaptiveDialog, + AdaptiveDialogContent, + AdaptiveDialogDescription, + AdaptiveDialogHeader, + AdaptiveDialogTitle, + AdaptiveDialogTrigger, +} from "@/components/ui/adaptive-dialog" import { Button } from "@/components/ui/button" interface LeaveChannelProps { @@ -16,24 +13,21 @@ interface LeaveChannelProps { } export function LeaveChannelButton({ onConfirm }: LeaveChannelProps) { return ( - - + + - - - - + + + + Are you sure? - - + + If you want to rejoin the channel, make sure to save a ticket first by clicking the Invite button. - - - - Cancel - Leave channel - - - + + + + + ) } diff --git a/browser-chat/frontend/src/components/logview.tsx b/browser-chat/frontend/src/components/logview.tsx index 29d6de1e..808a55d7 100644 --- a/browser-chat/frontend/src/components/logview.tsx +++ b/browser-chat/frontend/src/components/logview.tsx @@ -1,14 +1,50 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { Button } from "@/components/ui/button" +import { + AdaptiveDialog, + AdaptiveDialogContent, + AdaptiveDialogHeader, + AdaptiveDialogTitle, + AdaptiveDialogTrigger, +} from "@/components/ui/adaptive-dialog" import { useEffect, useState } from "react"; import { log, LogMessage } from "@/lib/log"; +import { FileText } from "lucide-react"; +import { useIsDesktop } from "@/hooks/use-media-query"; +import clsx from "clsx"; -interface LogViewProps { - onClose: () => void +export function LogViewButton() { + const [open, setOpen] = useState(false) + const isDesktop = useIsDesktop() + const cls = clsx( + isDesktop ? 'w-2xl max-w-3xl' : 'max-w-[100vw]', + "max-h-[80vh]" + ) + return ( + + + + + +
+ + Logs + +
+ +
+
+
+
+ ) } -export default function LogView({ onClose }: LogViewProps) { +export function LogView() { const logs = useLogs(); + const formatTimestamp = (date: Date) => { return date.toTimeString().split(" ")[0] + "." + date.getMilliseconds().toString().padStart(3, "0").slice(0, 2) } @@ -25,20 +61,16 @@ export default function LogView({ onClose }: LogViewProps) { } return ( -
-
-

Log View

- -
- {logs.map((log, index) => ( -
- {formatTimestamp(log.timestamp)} {log.message} -
- ))} -
-
- -
+
+ +
+ {logs.map((log, index) => ( +
+ {formatTimestamp(log.timestamp)} {log.message} +
+ ))} +
+
) } diff --git a/browser-chat/frontend/src/components/ui/adaptive-dialog.tsx b/browser-chat/frontend/src/components/ui/adaptive-dialog.tsx new file mode 100644 index 00000000..29b49288 --- /dev/null +++ b/browser-chat/frontend/src/components/ui/adaptive-dialog.tsx @@ -0,0 +1,104 @@ + +"use client" + +import * as React from "react" +import { useIsDesktop } from "@/hooks/use-media-query" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +const AdaptiveDialogContext = React.createContext<{ + isDesktop: boolean +}>({ isDesktop: true }) + +export function AdaptiveDialog({ children, ...props }: React.ComponentProps) { + const isDesktop = useIsDesktop() + + if (isDesktop) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} + +export function AdaptiveDialogTrigger({ children }: { children: React.ReactNode }) { + const { isDesktop } = React.useContext(AdaptiveDialogContext) + + if (isDesktop) { + return {children} + } + + return {children} +} + +export function AdaptiveDialogContent({ children, ...props }: React.ComponentProps) { + const { isDesktop } = React.useContext(AdaptiveDialogContext) + + if (isDesktop) { + return {children} + } + + return ( + + {children} + + + + + + + ) +} + +export function AdaptiveDialogHeader({ children, ...props }: React.ComponentProps) { + const { isDesktop } = React.useContext(AdaptiveDialogContext) + + if (isDesktop) { + return {children} + } + + return {children} +} + +export function AdaptiveDialogTitle({ children, ...props }: React.ComponentProps) { + const { isDesktop } = React.useContext(AdaptiveDialogContext) + + if (isDesktop) { + return {children} + } + + return {children} +} + +export function AdaptiveDialogDescription({ children, ...props }: React.ComponentProps) { + const { isDesktop } = React.useContext(AdaptiveDialogContext) + + if (isDesktop) { + return {children} + } + + return {children} +} diff --git a/browser-chat/frontend/src/components/ui/drawer.tsx b/browser-chat/frontend/src/components/ui/drawer.tsx new file mode 100644 index 00000000..c17b0cca --- /dev/null +++ b/browser-chat/frontend/src/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/browser-chat/frontend/src/components/ui/toggle.tsx b/browser-chat/frontend/src/components/ui/toggle.tsx new file mode 100644 index 00000000..0da5cb8b --- /dev/null +++ b/browser-chat/frontend/src/components/ui/toggle.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-3 min-w-10", + sm: "h-9 px-2.5 min-w-9", + lg: "h-11 px-5 min-w-11", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +const Toggle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, ...props }, ref) => ( + +)) + +Toggle.displayName = TogglePrimitive.Root.displayName + +export { Toggle, toggleVariants } diff --git a/browser-chat/frontend/src/hooks/use-media-query.ts b/browser-chat/frontend/src/hooks/use-media-query.ts new file mode 100644 index 00000000..807e1027 --- /dev/null +++ b/browser-chat/frontend/src/hooks/use-media-query.ts @@ -0,0 +1,24 @@ +import * as React from "react" + +export function useMediaQuery(query: string) { + const [value, setValue] = React.useState(false) + + React.useEffect(() => { + function onChange(event: MediaQueryListEvent) { + setValue(event.matches) + } + + const result = matchMedia(query) + result.addEventListener("change", onChange) + setValue(result.matches) + + return () => result.removeEventListener("change", onChange) + }, [query]) + + return value +} + +export function useIsDesktop() { + const isDesktop = useMediaQuery("(min-width: 768px)") + return isDesktop +} diff --git a/browser-chat/frontend/src/lib/log.ts b/browser-chat/frontend/src/lib/log.ts index 26c8908d..4964839b 100644 --- a/browser-chat/frontend/src/lib/log.ts +++ b/browser-chat/frontend/src/lib/log.ts @@ -49,3 +49,6 @@ class LogSystem { } export const log = new LogSystem() +while (log.get().length < 30) { + log.info("foobar foo baz " + log.get().length) +} diff --git a/browser-chat/frontend/src/lib/mock.ts b/browser-chat/frontend/src/lib/mock.ts new file mode 100644 index 00000000..c5980d13 --- /dev/null +++ b/browser-chat/frontend/src/lib/mock.ts @@ -0,0 +1,170 @@ +// import { PeerInfo, PeerRole, type API, type ChannelInfo, type TicketOpts } from "./api" +// import { log } from "./log" + +// export class MockAPI implements API { +// private channels: Map = +// new Map() +// private peers: PeerInfo[] = [ +// { id: "peer1", name: "Alice", status: "online", lastSeen: new Date(), role: PeerRole.Myself }, +// { id: "peer2", name: "Bob", status: "away", lastSeen: new Date(Date.now() - 5 * 60 * 1000), role: PeerRole.RemoteNode }, +// { id: "peer3", name: "Charlie", status: "offline", lastSeen: new Date(Date.now() - 24 * 60 * 60 * 1000), role: PeerRole.RemoteNode }, +// ] +// private messageSubscribers: Map void>> = +// new Map() +// private peerSubscribers: Map< +// string, +// Set<(peers: PeerInfo[]) => void> +// > = new Map() + +// constructor() { +// // Simulate incoming messages every 2 seconds +// setInterval(() => { +// this.channels.forEach((channel, channelId) => { +// const newMessage = { +// id: Math.random().toString(36).substring(2, 15), +// sender: this.peers[Math.floor(Math.random() * this.peers.length)].name, +// content: `Random message ${Math.random().toString(36).substring(2, 8)}`, +// } +// channel.messages.push(newMessage) +// this.notifyMessageSubscribers(channelId, newMessage) +// log.info(`New message in channel ${channelId}: ${newMessage.content}`,) +// }) +// }, 2000) + +// // Simulate peer status changes every 5 seconds +// setInterval(() => { +// this.peers.forEach((peer) => { +// peer.status = Math.random() > 0.5 ? "online" : Math.random() > 0.5 ? "away" : "offline" +// peer.lastSeen = new Date() +// }) +// this.channels.forEach((_, channelId) => { +// this.notifyPeerSubscribers(channelId, this.peers) +// }) +// log.info("Peer statuses updated",) +// }, 5000) +// } +// getMyself(channelId: string): PeerInfo { +// throw new Error("Method not implemented."); +// } +// setNickname(channelId: string, nickname: string): void { +// throw new Error("Method not implemented."); +// } + +// async createChannel(name: string): Promise { +// const id = Math.random().toString(36).substring(2, 15) +// this.channels.set(id, { name, messages: [] }) +// log.info(`Channel created: ${name} with id: ${id}`,) +// return { id, name } +// } + +// async joinChannel(ticket: string): Promise { +// const channel = Array.from(this.channels.entries()).find(([_, c]) => c.name === ticket) +// if (!channel) { +// log.error(`Failed to join channel: Channel not found for ticket ${ticket}`) +// throw new Error("Channel not found") +// } +// const [id, { name }] = channel +// log.info(`Joined channel with id: ${id}`,) +// return { id, name } +// } + +// async sendMessage(channelId: string, message: string): Promise { +// const channel = this.channels.get(channelId) +// if (!channel) { +// log.error(`Failed to send message: Channel not found for ID ${channelId}`) +// throw new Error("Channel not found") +// } +// const newMessage = { +// id: Math.random().toString(36).substring(2, 15), +// sender: "You", +// content: message, +// } +// channel.messages.push(newMessage) +// this.notifyMessageSubscribers(channelId, newMessage) +// log.info(`Message sent in channel ${channelId}: ${message}`,) +// } + +// async getMessages(channelId: string): Promise<{ id: string; sender: string; content: string }[]> { +// const channel = this.channels.get(channelId) +// if (!channel) { +// log.error(`Failed to get messages: Channel not found for ID ${channelId}`) +// throw new Error("Channel not found") +// } +// log.info(`Retrieved messages for channel ${channelId}`,) +// return channel.messages +// } + +// async getPeers( +// _channelId: string, +// ): Promise { +// return this.peers +// } + +// subscribeToMessages( +// channelId: string, +// callback: (message: { id: string; sender: string; content: string }) => void, +// ): () => void { +// if (!this.messageSubscribers.has(channelId)) { +// this.messageSubscribers.set(channelId, new Set()) +// } +// this.messageSubscribers.get(channelId)!.add(callback) +// log.info(`Subscribed to messages for channel ${channelId}`,) +// return () => { +// this.messageSubscribers.get(channelId)?.delete(callback) +// log.info(`Unsubscribed from messages for channel ${channelId}`,) +// } +// } + +// subscribeToPeers( +// channelId: string, +// callback: (peers: PeerInfo[]) => void, +// ): () => void { +// if (!this.peerSubscribers.has(channelId)) { +// this.peerSubscribers.set(channelId, new Set()) +// } +// this.peerSubscribers.get(channelId)!.add(callback) +// log.info(`Subscribed to peers for channel ${channelId}`,) +// return () => { +// this.peerSubscribers.get(channelId)?.delete(callback) +// log.info(`Unsubscribed from peers for channel ${channelId}`,) +// } +// } + +// getTicket(channelId: string, _opts: TicketOpts): string { +// const channel = this.channels.get(channelId) +// if (!channel) { +// log.error(`Failed to get ticket: Channel not found for ID ${channelId}`) +// throw new Error("Channel not found") +// } +// // In a real implementation, this would generate a proper ticket based on the options +// const ticket = `${channel.name}-${Math.random().toString(36).substring(2, 8)}` +// log.info(`Generated ticket for channel ${channelId}: ${ticket}`,) +// return ticket +// } + +// async closeChannel(channelId: string): Promise { +// if (!this.channels.has(channelId)) { +// log.error(`Failed to close channel: Channel not found for ID ${channelId}`) +// throw new Error("Channel not found") +// } +// this.channels.delete(channelId) +// this.messageSubscribers.delete(channelId) +// this.peerSubscribers.delete(channelId) +// log.info(`Closed channel ${channelId}`,) +// } + +// private notifyMessageSubscribers(channelId: string, message: { id: string; sender: string; content: string }) { +// this.messageSubscribers.get(channelId)?.forEach((callback) => callback(message)) +// } + +// private notifyPeerSubscribers( +// channelId: string, +// peers: PeerInfo[], +// ) { +// this.peerSubscribers.get(channelId)?.forEach((callback) => callback(peers)) +// } + +// subscribeToNeighbors(_channelId: string, _callback: (neighbors: number) => void): () => void { +// return () => { } +// } +// } From 0b03988b097edbb6d36c964578e03deda5db2cad Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 25 Feb 2025 11:06:17 +0100 Subject: [PATCH 2/4] fixup --- browser-chat/frontend/src/lib/log.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/browser-chat/frontend/src/lib/log.ts b/browser-chat/frontend/src/lib/log.ts index 4964839b..26c8908d 100644 --- a/browser-chat/frontend/src/lib/log.ts +++ b/browser-chat/frontend/src/lib/log.ts @@ -49,6 +49,3 @@ class LogSystem { } export const log = new LogSystem() -while (log.get().length < 30) { - log.info("foobar foo baz " + log.get().length) -} From 61b76c9ce13720588657baac458045cae1f99289 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 25 Feb 2025 11:20:58 +0100 Subject: [PATCH 3/4] cleanup --- browser-chat/frontend/src/app.tsx | 39 ++++--------- .../frontend/src/components/homescreen.tsx | 58 ++++++++----------- .../frontend/src/components/invitepopup.tsx | 15 ++++- browser-chat/frontend/src/lib/api.ts | 6 +- browser-chat/frontend/src/lib/iroh.ts | 4 +- 5 files changed, 50 insertions(+), 72 deletions(-) diff --git a/browser-chat/frontend/src/app.tsx b/browser-chat/frontend/src/app.tsx index da720067..eaee7449 100644 --- a/browser-chat/frontend/src/app.tsx +++ b/browser-chat/frontend/src/app.tsx @@ -7,7 +7,6 @@ import Header from "./components/header" import Sidebar from "./components/sidebar" import { ThemeProvider } from "next-themes" import { API, initApi, type ChannelInfo } from "./lib/api" -import { generate as generateName } from 'yet-another-name-generator' import { log } from "./lib/log" import { useIsDesktop } from "./hooks/use-media-query" @@ -63,26 +62,27 @@ function App({ api }: AppProps) { const [currentView, setCurrentView] = useState<"home" | "chat">("home") const [channels, setChannels] = useState([]) const [activeChannel, setActiveChannel] = useState(null) - const [nickname, setNickname] = useState(generateName()) const [showSidebar, setShowSidebar] = useState(false) - const joinChannel = async (ticket: string) => { + const joinChannel = (ticket: string, nickname: string) => { try { - const channel = await api.joinChannel(ticket, nickname) + const channel = api.joinChannel(ticket, nickname) setChannels((prevChannels) => [...prevChannels, channel]) - setActiveChannel(channel.id) setCurrentView("chat") + setActiveChannel(channel.id) + setShowSidebar(true) } catch (error) { log.error("Failed to join channel", error) } } - const createChannel = async () => { + const createChannel = (nickname: string) => { try { - const channel = await api.createChannel(nickname) + const channel = api.createChannel(nickname) setChannels((prevChannels) => [...prevChannels, channel]) setActiveChannel(channel.id) setCurrentView("chat") + setShowSidebar(true) } catch (error) { log.error("Failed to create channel", error) } @@ -117,7 +117,7 @@ function App({ api }: AppProps) { return ( <> - {isDesktop && (currentView === "chat" || showSidebar) && ( + {isDesktop && (showSidebar) && ( {currentView === "home" && ( { - joinChannel(ticket) - setShowSidebar(false) - }} - onCreate={() => { - createChannel() - setShowSidebar(false) - }} + onJoin={joinChannel} + onCreate={createChannel} /> )} {currentView === "chat" && activeChannel && ( closeChannel(activeChannel)} /> )} - {/* {showInvitePopup && activeChannel && ( - { - console.log("openchange", x) - setShowInvitePopup(x) - }} - channel={channels.find((c) => c.id === activeChannel)?.name || ""} - getTicket={(opts) => api.getTicket(activeChannel!, opts)} - /> - )} */}
) diff --git a/browser-chat/frontend/src/components/homescreen.tsx b/browser-chat/frontend/src/components/homescreen.tsx index 3d3bd31a..e25b09fd 100644 --- a/browser-chat/frontend/src/components/homescreen.tsx +++ b/browser-chat/frontend/src/components/homescreen.tsx @@ -4,35 +4,33 @@ import { useState, type FormEvent } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { generate as generateName } from 'yet-another-name-generator' + interface HomeScreenProps { - name: string - onSetName: (name: string) => void - onJoin: (ticket: string) => void - onCreate: () => void + onJoin: (ticket: string, nickname: string) => void + onCreate: (nickname: string) => void } -export default function HomeScreen({ onJoin, onCreate, name, onSetName }: HomeScreenProps) { +export default function HomeScreen({ onJoin, onCreate }: HomeScreenProps) { const [ticket, setTicket] = useState(() => { const url = new URL(document.location.toString()) const ticket = url.searchParams.get("ticket") if (ticket?.startsWith("chat")) return ticket return "" }) - // const [channelName, setChannelName] = useState("") + + const [nickname, setNickname] = useState(generateName()) const handleJoin = (e: FormEvent) => { e.preventDefault() if (ticket.trim()) { - onJoin(ticket.trim()) + onJoin(ticket.trim(), nickname) } } const handleCreate = (e: FormEvent) => { e.preventDefault() - onCreate() - // if (channelName.trim()) { - // onCreate(channelName.trim()) - // } + onCreate(nickname) } return ( @@ -41,32 +39,22 @@ export default function HomeScreen({ onJoin, onCreate, name, onSetName }: HomeSc

Your name

- onSetName(e.target.value)} placeholder="Enter your name" /> + setNickname(e.target.value)} placeholder="Enter your name" />
- {name.length && ( - <> -
-

Join Channel

-
- setTicket(e.target.value)} placeholder="Enter ticket" /> - -
-
-
-

Create Channel

-
- {/* setChannelName(e.target.value)} - placeholder="Enter channel name" - /> */} - -
-
- - - )} +
+

Join Channel

+
+ setTicket(e.target.value)} placeholder="Enter ticket" /> + +
+
+
+

Create Channel

+
+ +
+
) diff --git a/browser-chat/frontend/src/components/invitepopup.tsx b/browser-chat/frontend/src/components/invitepopup.tsx index 14e01905..bc35ba36 100644 --- a/browser-chat/frontend/src/components/invitepopup.tsx +++ b/browser-chat/frontend/src/components/invitepopup.tsx @@ -76,6 +76,9 @@ export function InvitePopupContent({ channel, getTicket }: InvitePopupProps) { const cliCommand = `cargo run -- join ${ticket}` + const ticketUrlFull = ticketUrl(ticket) + const ticketUrlShort = ticketUrl(ticket.substring(0, 16)) + return ( <>
@@ -90,9 +93,15 @@ export function InvitePopupContent({ channel, getTicket }: InvitePopupProps) {

Join link

- - {ticketUrl(ticket.substring(0, 16))}... - +
+ + {ticketUrlShort}… + + +

Join from the command line

diff --git a/browser-chat/frontend/src/lib/api.ts b/browser-chat/frontend/src/lib/api.ts index 48be5835..cce292de 100644 --- a/browser-chat/frontend/src/lib/api.ts +++ b/browser-chat/frontend/src/lib/api.ts @@ -22,9 +22,9 @@ async function importAndInitOnce() { } export interface API { - createChannel(nickname: string): Promise - joinChannel(ticket: string, nickname: string): Promise - sendMessage(channelId: string, message: string): Promise + createChannel(nickname: string): ChannelInfo + joinChannel(ticket: string, nickname: string): ChannelInfo + sendMessage(channelId: string, message: string): void setNickname(channelId: string, nickname: string): void getMessages(channelId: string): Message[] getPeers(channelId: string): PeerInfo[] diff --git a/browser-chat/frontend/src/lib/iroh.ts b/browser-chat/frontend/src/lib/iroh.ts index c5778887..4ad08b0e 100644 --- a/browser-chat/frontend/src/lib/iroh.ts +++ b/browser-chat/frontend/src/lib/iroh.ts @@ -31,12 +31,12 @@ export class IrohAPI implements API { return new IrohAPI(chatNode) } - async createChannel(nickname: string): Promise { + createChannel(nickname: string): ChannelInfo { const channel = this.chatNode.create(nickname) return this.joinInner(channel, nickname) } - async joinChannel(ticket: string, nickname: string): Promise { + joinChannel(ticket: string, nickname: string): ChannelInfo { const channel = this.chatNode.join(ticket, nickname) return this.joinInner(channel, nickname) } From 11849d013e5c43bba28b15812f962209ef016e1f Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 25 Feb 2025 11:26:03 +0100 Subject: [PATCH 4/4] fixup --- browser-chat/frontend/src/lib/mock.ts | 170 -------------------------- 1 file changed, 170 deletions(-) delete mode 100644 browser-chat/frontend/src/lib/mock.ts diff --git a/browser-chat/frontend/src/lib/mock.ts b/browser-chat/frontend/src/lib/mock.ts deleted file mode 100644 index c5980d13..00000000 --- a/browser-chat/frontend/src/lib/mock.ts +++ /dev/null @@ -1,170 +0,0 @@ -// import { PeerInfo, PeerRole, type API, type ChannelInfo, type TicketOpts } from "./api" -// import { log } from "./log" - -// export class MockAPI implements API { -// private channels: Map = -// new Map() -// private peers: PeerInfo[] = [ -// { id: "peer1", name: "Alice", status: "online", lastSeen: new Date(), role: PeerRole.Myself }, -// { id: "peer2", name: "Bob", status: "away", lastSeen: new Date(Date.now() - 5 * 60 * 1000), role: PeerRole.RemoteNode }, -// { id: "peer3", name: "Charlie", status: "offline", lastSeen: new Date(Date.now() - 24 * 60 * 60 * 1000), role: PeerRole.RemoteNode }, -// ] -// private messageSubscribers: Map void>> = -// new Map() -// private peerSubscribers: Map< -// string, -// Set<(peers: PeerInfo[]) => void> -// > = new Map() - -// constructor() { -// // Simulate incoming messages every 2 seconds -// setInterval(() => { -// this.channels.forEach((channel, channelId) => { -// const newMessage = { -// id: Math.random().toString(36).substring(2, 15), -// sender: this.peers[Math.floor(Math.random() * this.peers.length)].name, -// content: `Random message ${Math.random().toString(36).substring(2, 8)}`, -// } -// channel.messages.push(newMessage) -// this.notifyMessageSubscribers(channelId, newMessage) -// log.info(`New message in channel ${channelId}: ${newMessage.content}`,) -// }) -// }, 2000) - -// // Simulate peer status changes every 5 seconds -// setInterval(() => { -// this.peers.forEach((peer) => { -// peer.status = Math.random() > 0.5 ? "online" : Math.random() > 0.5 ? "away" : "offline" -// peer.lastSeen = new Date() -// }) -// this.channels.forEach((_, channelId) => { -// this.notifyPeerSubscribers(channelId, this.peers) -// }) -// log.info("Peer statuses updated",) -// }, 5000) -// } -// getMyself(channelId: string): PeerInfo { -// throw new Error("Method not implemented."); -// } -// setNickname(channelId: string, nickname: string): void { -// throw new Error("Method not implemented."); -// } - -// async createChannel(name: string): Promise { -// const id = Math.random().toString(36).substring(2, 15) -// this.channels.set(id, { name, messages: [] }) -// log.info(`Channel created: ${name} with id: ${id}`,) -// return { id, name } -// } - -// async joinChannel(ticket: string): Promise { -// const channel = Array.from(this.channels.entries()).find(([_, c]) => c.name === ticket) -// if (!channel) { -// log.error(`Failed to join channel: Channel not found for ticket ${ticket}`) -// throw new Error("Channel not found") -// } -// const [id, { name }] = channel -// log.info(`Joined channel with id: ${id}`,) -// return { id, name } -// } - -// async sendMessage(channelId: string, message: string): Promise { -// const channel = this.channels.get(channelId) -// if (!channel) { -// log.error(`Failed to send message: Channel not found for ID ${channelId}`) -// throw new Error("Channel not found") -// } -// const newMessage = { -// id: Math.random().toString(36).substring(2, 15), -// sender: "You", -// content: message, -// } -// channel.messages.push(newMessage) -// this.notifyMessageSubscribers(channelId, newMessage) -// log.info(`Message sent in channel ${channelId}: ${message}`,) -// } - -// async getMessages(channelId: string): Promise<{ id: string; sender: string; content: string }[]> { -// const channel = this.channels.get(channelId) -// if (!channel) { -// log.error(`Failed to get messages: Channel not found for ID ${channelId}`) -// throw new Error("Channel not found") -// } -// log.info(`Retrieved messages for channel ${channelId}`,) -// return channel.messages -// } - -// async getPeers( -// _channelId: string, -// ): Promise { -// return this.peers -// } - -// subscribeToMessages( -// channelId: string, -// callback: (message: { id: string; sender: string; content: string }) => void, -// ): () => void { -// if (!this.messageSubscribers.has(channelId)) { -// this.messageSubscribers.set(channelId, new Set()) -// } -// this.messageSubscribers.get(channelId)!.add(callback) -// log.info(`Subscribed to messages for channel ${channelId}`,) -// return () => { -// this.messageSubscribers.get(channelId)?.delete(callback) -// log.info(`Unsubscribed from messages for channel ${channelId}`,) -// } -// } - -// subscribeToPeers( -// channelId: string, -// callback: (peers: PeerInfo[]) => void, -// ): () => void { -// if (!this.peerSubscribers.has(channelId)) { -// this.peerSubscribers.set(channelId, new Set()) -// } -// this.peerSubscribers.get(channelId)!.add(callback) -// log.info(`Subscribed to peers for channel ${channelId}`,) -// return () => { -// this.peerSubscribers.get(channelId)?.delete(callback) -// log.info(`Unsubscribed from peers for channel ${channelId}`,) -// } -// } - -// getTicket(channelId: string, _opts: TicketOpts): string { -// const channel = this.channels.get(channelId) -// if (!channel) { -// log.error(`Failed to get ticket: Channel not found for ID ${channelId}`) -// throw new Error("Channel not found") -// } -// // In a real implementation, this would generate a proper ticket based on the options -// const ticket = `${channel.name}-${Math.random().toString(36).substring(2, 8)}` -// log.info(`Generated ticket for channel ${channelId}: ${ticket}`,) -// return ticket -// } - -// async closeChannel(channelId: string): Promise { -// if (!this.channels.has(channelId)) { -// log.error(`Failed to close channel: Channel not found for ID ${channelId}`) -// throw new Error("Channel not found") -// } -// this.channels.delete(channelId) -// this.messageSubscribers.delete(channelId) -// this.peerSubscribers.delete(channelId) -// log.info(`Closed channel ${channelId}`,) -// } - -// private notifyMessageSubscribers(channelId: string, message: { id: string; sender: string; content: string }) { -// this.messageSubscribers.get(channelId)?.forEach((callback) => callback(message)) -// } - -// private notifyPeerSubscribers( -// channelId: string, -// peers: PeerInfo[], -// ) { -// this.peerSubscribers.get(channelId)?.forEach((callback) => callback(peers)) -// } - -// subscribeToNeighbors(_channelId: string, _callback: (neighbors: number) => void): () => void { -// return () => { } -// } -// }