// file: app/components/app-layout.tsx import React from "react"; import { useMatches } from "react-router"; import { useShallow } from "zustand/react/shallow"; import { useServerListStore } from "~/stores/server-list-store"; import { CreateServerButton } from "./custom-ui/create-server-button"; import { HomeButton } from "./custom-ui/home-button"; import { ServerButton } from "./custom-ui/server-button"; import UserStatus from "./custom-ui/user-status"; import { ScrollArea } from "./ui/scroll-area"; import { Separator } from "./ui/separator"; interface AppLayoutProps { children: React.ReactNode; } export default function AppLayout({ children }: AppLayoutProps) { const servers = Object.values(useServerListStore(useShallow((state) => state.servers))); const matches = useMatches(); const list = React.useMemo(() => { return matches .map( (match) => ( match.handle as { listComponent?: React.ReactNode; } )?.listComponent, ) .reverse() .find((component) => !!component); }, [matches]); return (
{list}
{children}
); } // file: app/components/channel-area.tsx import { useInfiniteQuery, type QueryFunctionContext } from "@tanstack/react-query"; import type { Channel, Message, MessageId } from "~/lib/api/types"; import ChatMessage from "./chat-message"; import MessageBox from "./message-box"; import { Separator } from "./ui/separator"; import VisibleTrigger from "./visible-trigger"; interface ChannelAreaProps { channel: Channel; } export default function ChannelArea({ channel }: ChannelAreaProps) { const channelId = channel.id; const fetchMessages = async ({ pageParam }: QueryFunctionContext) => { return await import("~/lib/api/client/channel").then((m) => m.default.paginatedMessages(channelId, 50, pageParam as MessageId | undefined), ); }; const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isPending, status } = useInfiniteQuery({ queryKey: ["messages", channelId], initialPageParam: undefined, queryFn: fetchMessages, getNextPageParam: (lastPage) => (lastPage.length < 50 ? undefined : lastPage[lastPage.length - 1]?.id), staleTime: Infinity, }); const fetchNextPageVisible = () => { if (!isFetchingNextPage && hasNextPage) fetchNextPage(); }; let messageArea = null; if (isPending) { messageArea = (
Loading...
); } else { messageArea = ( <>
{status === "success" && renderMessages(data.pages)}
); } return ( <>
{channel?.name}
{messageArea}
); } function renderMessages(pages: Message[][]) { const messageElements: React.ReactNode[] = []; let lastDate: string | null = null; const formatMessageDate = (date: Date) => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); if (messageDate.getTime() === today.getTime()) { const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto", }); return capitalize(rtf.format(0, "day")); } else if (messageDate.getTime() === yesterday.getTime()) { const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto", }); return capitalize(rtf.format(-1, "day")); } else { return date.toLocaleDateString(undefined, { month: "short", day: "numeric", }); } }; pages.forEach((page) => { page.forEach((message) => { const messageDate = message.createdAt.toDateString(); if (messageDate != lastDate) { if (lastDate) messageElements.push(
{formatMessageDate(new Date(lastDate))}
, ); lastDate = messageDate; } messageElements.push(
, ); }); }); return messageElements; } // file: app/components/chat-message-attachment.tsx import { Download, ExternalLink, Maximize } from "lucide-react"; import { AspectRatio } from "~/components/ui/aspect-ratio"; // Shadcn UI AspectRatio import { Button } from "~/components/ui/button"; // Shadcn UI Button import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "~/components/ui/dialog"; // Shadcn UI Dialog import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; // Shadcn UI Tooltip import type { UploadedFile } from "~/lib/api/types"; // Adjust path as needed import { formatFileSize } from "~/lib/utils"; // Adjust path import { FileIcon } from "./file-icon"; interface ChatMessageAttachmentProps { file: UploadedFile; } export default function ChatMessageAttachment({ file }: ChatMessageAttachmentProps) { if (file.contentType.startsWith("image/")) { return ; } return ; } function GenericFileAttachment({ file }: ChatMessageAttachmentProps) { return (

{file.filename}

{formatFileSize(file.size)}

Download Open in new tab
); } function ImageAttachment({ file }: ChatMessageAttachmentProps) { return (
{file.filename}

{file.filename} ({formatFileSize(file.size)})

{file.filename}
{file.filename}
); } // file: app/components/chat-message.tsx import { Clock } from "lucide-react"; import { useShallow } from "zustand/react/shallow"; import { useFetchUser } from "~/hooks/use-fetch-user"; import type { Message } from "~/lib/api/types"; import { useUsersStore } from "~/stores/users-store"; import ChatMessageAttachment from "./chat-message-attachment"; import UserAvatar from "./user-avatar"; import UserContextMenu from "./user-context-menu"; interface ChatMessageProps { message: Message; } export default function ChatMessage({ message }: ChatMessageProps) { const { user } = useUsersStore( useShallow((state) => ({ user: state.users[message.authorId], })), ); useFetchUser(message.authorId); const formatMessageDate = (date: Date) => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); // Get localized time string const timeString = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false, }); const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); if (messageDate.getTime() === today.getTime()) { // Use Intl.RelativeTimeFormat for localized "Today" const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto", }); return `${capitalize(rtf.format(0, "day"))}, ${timeString}`; } else if (messageDate.getTime() === yesterday.getTime()) { // Use Intl.RelativeTimeFormat for localized "Yesterday" const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto", }); return `${capitalize(rtf.format(-1, "day"))}, ${timeString}`; } else { return date.toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, }); } }; return (
{user?.displayName || user?.username}
{formatMessageDate(message.createdAt)}
{message.content}
{message.attachments.map((file) => (
))}
); } // file: app/components/custom-ui/channel-list-item.tsx import { ChevronDown, Hash, Volume2 } from "lucide-react"; import React from "react"; import { NavLink } from "react-router"; import { useShallow } from "zustand/react/shallow"; import { useFetchUsers } from "~/hooks/use-fetch-user"; import { deleteChannel } from "~/lib/api/client/server"; import { ChannelType, type ServerChannel } from "~/lib/api/types"; import { cn } from "~/lib/utils"; import { useChannelsVoiceStateStore } from "~/stores/channels-voice-state"; import { useGatewayStore } from "~/stores/gateway-store"; import { useUsersStore } from "~/stores/users-store"; import { useVoiceStateStore } from "~/stores/voice-state-store"; import { Button } from "../ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "../ui/context-menu"; import UserAvatar from "../user-avatar"; import UserContextMenu from "../user-context-menu"; interface ChannelListItemProps { channel: ServerChannel; } const onDeleteChannel = async (channel: ServerChannel) => { await deleteChannel(channel.serverId, channel.id); }; interface ChannelItemWrapperProps { channel: ServerChannel; icon: React.ReactNode; onButtonClick?: () => void; // For direct button clicks (e.g., join voice) navTo?: string; // If provided, the button becomes a NavLink isExplicitlyActive?: boolean; // To set active state externally (e.g., current voice channel) } function ChannelItemWrapper({ channel, icon, onButtonClick, navTo, isExplicitlyActive }: ChannelItemWrapperProps) { const buttonInnerContent = (
{icon}
{channel.name}
); const renderButton = (isNavLinkActive?: boolean) => { // Active state priority: explicit prop > NavLink state > default (false) const isActive = isExplicitlyActive ?? isNavLinkActive ?? false; return ( ); }; let triggerContent: React.ReactNode; if (navTo) { triggerContent = ( {({ isActive: navLinkIsActive }) => renderButton(navLinkIsActive)} ); } else { // For non-NavLink cases (like voice channel), active state is determined by isExplicitlyActive triggerContent = renderButton(); } return ( {triggerContent} onDeleteChannel(channel)}> Delete ); } function ServerCategory({ channel }: ChannelListItemProps) { return (
{channel.name}
); } function ServerVoice({ channel }: ChannelListItemProps) { const updateVoiceState = useGatewayStore((state) => state.updateVoiceState); const voiceStateChannel = useVoiceStateStore((state) => state.activeChannel); const channelVoiceState = useChannelsVoiceStateStore((state) => state.channels[channel.id]) || {}; const userIds = Object.keys(channelVoiceState.users ?? {}); useFetchUsers(userIds); const users = useUsersStore(useShallow((state) => state.users)); const channelUsers = React.useMemo(() => userIds.map((userId) => users[userId]).filter(Boolean), [userIds, users]); const isCurrentVoiceChannel = voiceStateChannel?.serverId === channel.serverId && voiceStateChannel.channelId === channel.id; const handleJoinVoiceChannel = () => { if (isCurrentVoiceChannel) return; updateVoiceState(channel.serverId, channel.id); }; return ( <> } onButtonClick={handleJoinVoiceChannel} isExplicitlyActive={isCurrentVoiceChannel} /> {channelUsers.length > 0 && (
{" "} {/* Added py-1 for spacing */} {channelUsers.map((user) => (
{" "} {/* Added padding and hover for better UX */} {user.displayName || user.username}
))}
)} ); } function ServerText({ channel }: ChannelListItemProps) { return ( } navTo={`/app/server/${channel.serverId}/${channel.id}`} /> ); } export default function ServerChannelListItem({ channel }: ChannelListItemProps) { switch (channel.type) { case ChannelType.SERVER_CATEGORY: return ; case ChannelType.SERVER_VOICE: return ; case ChannelType.SERVER_TEXT: return ; default: return null; } } // file: app/components/custom-ui/create-server-button.tsx import { CirclePlus } from "lucide-react"; import { Button } from "~/components/ui/button"; import { ModalType, useModalStore } from "~/stores/modal-store"; export function CreateServerButton() { const onOpen = useModalStore((state) => state.onOpen); return ( ); } // file: app/components/custom-ui/home-button.tsx import { NavLink } from "react-router"; import Discord from "../icons/Discord"; import { Button } from "../ui/button"; export function HomeButton() { return ( {({ isActive }) => ( )} ); } // file: app/components/custom-ui/online-status.tsx import { Circle, MinusCircle, Moon } from "lucide-react"; import { cn } from "~/lib/utils"; // Assuming you have a cn utility // Define status type for better type safety if used elsewhere export type UserStatusType = "online" | "dnd" | "idle" | "offline"; interface OnlineStatusProps extends React.ComponentProps<"div"> { status: UserStatusType; } export function OnlineStatus({ status, className, ...rest }: OnlineStatusProps) { const statusTitle = status.charAt(0).toUpperCase() + status.slice(1); let statusIndicatorIcon: React.ReactNode = null; switch (status) { case "online": statusIndicatorIcon = ; break; case "dnd": statusIndicatorIcon = ( ); break; case "idle": statusIndicatorIcon = ; break; case "offline": statusIndicatorIcon = ; break; } return (
{statusIndicatorIcon}
); } // file: app/components/custom-ui/password-input.tsx import { EyeIcon, EyeOffIcon } from "lucide-react"; import React from "react"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; export function PasswordInput(props: React.ComponentProps<"input">) { const [showPassword, setShowPassword] = React.useState(false); const disabled = props.value === "" || props.value === undefined || props.disabled; return (
); } // file: app/components/custom-ui/private-channel-list-item.tsx import { Check } from "lucide-react"; import { NavLink } from "react-router"; import { useShallow } from "zustand/react/shallow"; import { useFetchUsers } from "~/hooks/use-fetch-user"; import type { RecipientChannel } from "~/lib/api/types"; import { cn } from "~/lib/utils"; import { useUsersStore } from "~/stores/users-store"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import UserAvatar from "../user-avatar"; import { OnlineStatus } from "./online-status"; interface PrivateChannelListItemProps { channel: RecipientChannel; } export default function PrivateChannelListItem({ channel }: PrivateChannelListItemProps) { const currentUserId = useUsersStore((state) => state.currentUserId); const recipients = channel.recipients.filter((recipient) => recipient !== currentUserId); useFetchUsers(recipients); const recipientsUsers = useUsersStore( useShallow((state) => recipients.map((recipient) => state.users[recipient]).filter(Boolean)), ); const renderSystemBadge = recipientsUsers.some((recipient) => recipient.system) && recipients.length === 1; return ( <> {({ isActive }) => ( )} ); } // file: app/components/custom-ui/server-button.tsx import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; import { NavLink } from "react-router"; import type { Server } from "~/lib/api/types"; import { cn, getFirstLetters } from "~/lib/utils"; import { Button } from "../ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; export interface ServerButtonProps { server: Server; } export function ServerButton({ server }: ServerButtonProps) { return ( {({ isActive }) => (

{server.name}

)}
); } // file: app/components/custom-ui/settings-button.tsx import { Settings } from "lucide-react"; import { useNavigate } from "react-router"; import { useTokenStore } from "~/stores/token-store"; import { Button } from "../ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; export function SettingsButton() { const setToken = useTokenStore((state) => state.setToken); const navigate = useNavigate(); const onOpenSettings = () => { navigate("/app/settings"); }; const onLogout = () => { setToken(undefined); window.location.reload(); }; return ( Settings Logout ); } // file: app/components/custom-ui/text-box.tsx import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; import { cn } from "~/lib/utils"; export interface TextBoxProps extends Omit, "onChange" | "value"> { value: string; onChange: (value: string) => void; placeholder?: string; wrapperClassName?: string; inputClassName?: string; disabled?: boolean; autoFocus?: boolean; spellCheck?: boolean; } export const TextBox = forwardRef( ( { value, onChange, placeholder, wrapperClassName, inputClassName, disabled = false, autoFocus = false, spellCheck = true, onInput, onBlur, onFocus, ...rest }, ref, ) => { const localRef = useRef(null); useImperativeHandle(ref, () => localRef.current as HTMLDivElement); // Function to handle DOM updates const updateDOM = (newValue: string) => { if (localRef.current) { // Only update if different to avoid selection issues if (localRef.current.textContent !== newValue) { localRef.current.textContent = newValue; // Clear any
elements if the content is empty if (!newValue && localRef.current.innerHTML.includes("
")) { localRef.current.innerHTML = ""; } } } }; // Update DOM when value prop changes useEffect(() => { updateDOM(value); }, [value]); useEffect(() => { if (autoFocus && localRef.current) { localRef.current.focus(); } }, [autoFocus]); const handleInput = (event: React.FormEvent) => { const newValue = event.currentTarget.textContent || ""; // Handle the case where the content is empty but contains a
if (!newValue && event.currentTarget.innerHTML.includes("
")) { event.currentTarget.innerHTML = ""; } onChange(newValue); onInput?.(event); }; const handlePaste = (event: React.ClipboardEvent) => { event.preventDefault(); const text = event.clipboardData.getData("text/plain"); // Use document.execCommand to maintain undo stack document.execCommand("insertText", false, text); // Manually trigger input event const inputEvent = new Event("input", { bubbles: true }); event.currentTarget.dispatchEvent(inputEvent); }; return (
localRef.current?.focus()} >
); }, ); TextBox.displayName = "TextBox"; export default TextBox; // file: app/components/custom-ui/user-status.tsx import { PhoneMissed, Signal } from "lucide-react"; import { useShallow } from "zustand/react/shallow"; import { useServerChannelsStore } from "~/stores/server-channels-store"; import { useServerListStore } from "~/stores/server-list-store"; import { useUsersStore } from "~/stores/users-store"; import { useVoiceStateStore } from "~/stores/voice-state-store"; import { ThemeToggle } from "../theme/theme-toggle"; import { Button } from "../ui/button"; import { Separator } from "../ui/separator"; import UserAvatar from "../user-avatar"; import { OnlineStatus } from "./online-status"; import { SettingsButton } from "./settings-button"; function VoiceStatus({ voiceState }: { voiceState: { serverId: string; channelId: string } }) { const leaveVoiceChannel = () => { useVoiceStateStore.getState().leaveVoiceChannel(); }; const channel = useServerChannelsStore( useShallow((state) => state.channels[voiceState.serverId]?.[voiceState.channelId]), ); const server = useServerListStore(useShallow((state) => state.servers[voiceState.serverId])); return (
{channel?.name || "Unknown channel"} / {server?.name}
); } export default function UserStatus() { const user = useUsersStore((state) => state.getCurrentUser()!); const voiceState = useVoiceStateStore((state) => state.activeChannel); return (
{voiceState && ( <> )}
{user?.displayName || user?.username || "Unknown user"}
@{user?.username}
{/* */} {/* */}
); } // file: app/components/file-icon.tsx import { FileArchive, FileAudio, FileImage, FileQuestion, FileSpreadsheet, FileText, FileVideo, type LucideProps, } from "lucide-react"; interface FileIconProps extends LucideProps { contentType: string; className?: string; } export function FileIcon({ contentType, className, ...props }: FileIconProps) { const commonProps = { className: className ?? "h-5 w-5", ...props }; if (contentType.startsWith("image/")) { return ; } if (contentType.startsWith("audio/")) { return ; } if (contentType.startsWith("video/")) { return ; } if (contentType === "application/pdf") { return ; } if ( contentType.startsWith("application/vnd.ms-excel") || contentType.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml") ) { return ; // Could use a specific Excel icon if available/desired } if ( contentType.startsWith("application/msword") || contentType.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml") ) { return ; } if ( contentType.startsWith("application/vnd.ms-powerpoint") || contentType.startsWith("application/vnd.openxmlformats-officedocument.presentationml") ) { return ; } if ( contentType === "application/zip" || contentType === "application/x-rar-compressed" || contentType === "application/x-7z-compressed" || contentType === "application/gzip" || contentType === "application/x-tar" ) { return ; } if (contentType.startsWith("text/")) { return ; } return ; // Default for unknown types } // file: app/components/icons/Discord.tsx import type { SVGProps } from "react"; const Discord = (props: SVGProps) => ( ); export default Discord; // file: app/components/manager/gateway-websocket-connection-manager.tsx import { useQueryClient } from "@tanstack/react-query"; import { useEffect } from "react"; import { ConnectionState } from "~/lib/websocket/gateway/types"; import { useGatewayStore } from "~/stores/gateway-store"; import { useTokenStore } from "~/stores/token-store"; export function GatewayWebSocketConnectionManager() { const token = useTokenStore((state) => state.token); const setQueryClient = useGatewayStore((state) => state.setQueryClient); const queryClient = useQueryClient(); useEffect(() => { setQueryClient(queryClient); }, [queryClient]); useEffect(() => { const { status, connect, disconnect } = useGatewayStore.getState(); if (token) { connect(token); } else { if (status === ConnectionState.CONNECTED) { disconnect(); } } return () => { if (status === ConnectionState.CONNECTED) { disconnect(); } }; }, [token]); return null; } // file: app/components/manager/webrtc-connection-manager.tsx import { useEffect, useRef } from "react"; import { ConnectionState } from "~/lib/websocket/voice/types"; import { useGatewayStore } from "~/stores/gateway-store"; import { useVoiceStateStore } from "~/stores/voice-state-store"; import { useWebRTCStore } from "~/stores/webrtc-store"; export function WebRTCConnectionManager() { const gateway = useGatewayStore(); const voiceState = useVoiceStateStore(); const webrtc = useWebRTCStore(); const remoteStream = useWebRTCStore((state) => state.remoteStream); const audioRef = useRef(null); if (audioRef.current) { audioRef.current.srcObject = remoteStream; } useEffect(() => { const unsubscribe = gateway.onVoiceServerUpdate(async (event) => { await webrtc.connect(event.token); voiceState.joinVoiceChannel(event.serverId, event.channelId); const stream = await navigator.mediaDevices.getUserMedia({ audio: { noiseSuppression: false, }, video: false, }); webrtc.createOffer(stream); }); return () => { voiceState.leaveVoiceChannel(); unsubscribe(); }; }, []); useEffect(() => { if (webrtc.status === ConnectionState.DISCONNECTED) { voiceState.leaveVoiceChannel(); } }, [webrtc.status]); return ( <>