Merge pull request #8 from kleo-dev/patches-and-improvements

Patches and improvements
This commit is contained in:
Klesti Selimaj
2025-10-13 14:05:29 +02:00
committed by GitHub
12 changed files with 272 additions and 170 deletions
+18 -17
View File
@@ -5,7 +5,7 @@ import MessageBox from "@/components/MessageBox";
import AppSidebar from "@/components/Sidebar"; import AppSidebar from "@/components/Sidebar";
import { Message } from "@/types/types"; import { Message } from "@/types/types";
import useUser, { User } from "@/hooks/get-user"; import useUser, { User } from "@/hooks/get-user";
import { NumberMap } from "@/types/typeUtils"; import { NumberMap, StringMap } from "@/types/typeUtils";
import auth, { makeAddress } from "@/lib/auth"; import auth, { makeAddress } from "@/lib/auth";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
@@ -14,36 +14,37 @@ import { useParams } from "next/navigation";
export default function DMs() { export default function DMs() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const numericId = id ? Number(id) : undefined;
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const { user, loading } = useUser(); const [userList, setUserList] = useState<StringMap<User>>({});
const [userList, setUserList] = useState<NumberMap<User>>({}); const [user, setUser] = useState<User | undefined>();
useEffect(() => {
if (!numericId || !user || loading) return;
auth(makeAddress("localhost", 7090), wsRef, () => {}, setMessages);
setUserList((prev) => {
prev[user.id] = user;
return prev;
});
}, [id, user, loading]);
return ( return (
<AppSidebar user={user}> <AppSidebar
setMessages={setMessages}
wsRef={wsRef}
userList={userList}
setUserList={setUserList}
chatWith={id}
setUser={setUser}
>
<MessageBox <MessageBox
channelName="Alice" channelName={userList[id]?.display_name || id}
userList={userList} userList={userList}
setUserList={setUserList} setUserList={setUserList}
messages={messages messages={messages
.filter((m) => m.channel_id === numericId || m.from === numericId) .filter(
(m) =>
(m.channel_id === id && m.from === user?.id) ||
(m.from === id && m.channel_id === user?.id)
)
.toReversed()} .toReversed()}
sendMessage={(message) => { sendMessage={(message) => {
wsRef.current?.send( wsRef.current?.send(
JSON.stringify({ JSON.stringify({
type: "send_message", type: "send_message",
params: { params: {
channel_id: numericId, channel_id: id,
contents: message, contents: message,
}, },
}) })
+21 -34
View File
@@ -1,60 +1,47 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useRef, useState } from "react";
import MessageBox from "@/components/MessageBox";
import AppSidebar from "@/components/Sidebar"; import AppSidebar from "@/components/Sidebar";
import { Message } from "@/types/types"; import { User } from "@/hooks/get-user";
import useUser, { User } from "@/hooks/get-user"; import { StringMap } from "@/types/typeUtils";
import { NumberMap } from "@/types/typeUtils";
import auth, { makeAddress } from "@/lib/auth";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { toast } from "sonner";
import ProfilePicture from "@/components/ProfilePicture"; import ProfilePicture from "@/components/ProfilePicture";
export default function Chat() { export default function Chat() {
const ip = "localhost";
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const { user, loading } = useUser(); const [userList, setUserList] = useState<StringMap<User>>({});
const [userList, setUserList] = useState<NumberMap<User>>({});
useEffect(() => {
if (!ip || !user || loading) return;
auth(
makeAddress(ip, 7090),
wsRef,
() => {},
() => {},
(m) => {
toast(`New message from ${m.from}`, {
description: m.contents.slice(0, 80),
});
}
);
setUserList((prev) => {
prev[user.id] = user;
return prev;
});
}, [ip, user, loading]);
return ( return (
<AppSidebar user={{ id: 173, username: "leo", email: "" }}> <AppSidebar
wsRef={wsRef}
userList={userList}
setUserList={setUserList}
setMessages={() => {}}
>
<Card className="bg-background w-full"> <Card className="bg-background w-full">
<CardHeader> <CardHeader>
<CardTitle>Direct Messages</CardTitle> <CardTitle>Direct Messages</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<DMItem name="Alice" status="online" /> <DMItem name="Alice" status="online" avatar="" />
</CardContent> </CardContent>
</Card> </Card>
</AppSidebar> </AppSidebar>
); );
} }
function DMItem({ name, status }: { name: string; status: string }) { function DMItem({
name,
avatar,
status,
}: {
name: string;
avatar: string;
status: string;
}) {
return ( return (
<Card className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent"> <Card className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent">
<ProfilePicture name={name} /> <ProfilePicture name={name} url={avatar} />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium">{name}</span> <span className="text-sm font-medium">{name}</span>
<span className="text-xs text-muted-foreground">{status}</span> <span className="text-xs text-muted-foreground">{status}</span>
@@ -5,36 +5,30 @@ import { useParams } from "next/navigation";
import MessageBox from "@/components/MessageBox"; import MessageBox from "@/components/MessageBox";
import AppSidebar from "@/components/Sidebar"; import AppSidebar from "@/components/Sidebar";
import { Message, Server as ServerType } from "@/types/types"; import { Message, Server as ServerType } from "@/types/types";
import useUser, { User } from "@/hooks/get-user"; import { User } from "@/hooks/get-user";
import { NumberMap } from "@/types/typeUtils"; import { StringMap } from "@/types/typeUtils";
import auth, { makeAddress } from "@/lib/auth"; import auth, { makeAddress } from "@/lib/auth";
export default function Server() { export default function Server() {
const { id: ip } = useParams<{ id: string }>(); const { ip } = useParams<{ ip: string }>();
const wsServerRef = useRef<WebSocket | null>(null);
const wsNodeRef = useRef<WebSocket | null>(null);
const [server, setServer] = useState<undefined | ServerType>(); const [server, setServer] = useState<undefined | ServerType>();
const wsRef = useRef<WebSocket | null>(null);
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const { user, loading } = useUser(); const [userList, setUserList] = useState<StringMap<User>>({});
const [userList, setUserList] = useState<NumberMap<User>>({});
useEffect(() => { useEffect(() => {
if (!ip || !user || loading) return; if (!ip) return;
auth(makeAddress(ip), wsRef, setServer, setMessages); auth(makeAddress(ip, 7080), wsServerRef, setServer, setMessages);
setUserList((prev) => { }, [ip]);
prev[user.id] = user;
return prev;
});
}, [ip, user, loading]);
return ( return (
<AppSidebar <AppSidebar
user={user} wsRef={wsNodeRef}
server={ setMessages={setMessages}
{ userList={userList}
channels: [{ id: "general", name: "General", kind: "text" }], setUserList={setUserList}
...server, server={server}
} as ServerType
}
> >
<MessageBox <MessageBox
channelName="General" channelName="General"
@@ -44,7 +38,7 @@ export default function Server() {
.filter((m) => m.channel_id === "general") .filter((m) => m.channel_id === "general")
.toReversed()} .toReversed()}
sendMessage={(message) => { sendMessage={(message) => {
wsRef.current?.send( wsServerRef.current?.send(
JSON.stringify({ JSON.stringify({
type: "send_message", type: "send_message",
params: { params: {
-66
View File
@@ -1,66 +0,0 @@
"use client";
import { useProfileSettings, useClientSettings } from "@/hooks/use-settings";
import { setClientSettings } from "@/lib/clientSettings";
import Settings from "./settings";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { CardContent } from "@/components/ui/card";
import { PROFILE_SCHEMA, CLIENT_SCHEMA } from "@/types/settings";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { ChevronLeftCircle } from "lucide-react";
import axios from "axios";
export default function SettingsPage() {
const [settings, setSettings] = useClientSettings();
const [profileSettings, setProfileSettings] = useProfileSettings();
const router = useRouter();
return (
<div>
<Button
variant="outline"
className="ml-2 mt-2"
onClick={() => router.back()}
>
<ChevronLeftCircle />
</Button>
<Tabs className="mx-auto mt-3" defaultValue="client">
<TabsList className="mx-auto">
<TabsTrigger value="client">Client Settings</TabsTrigger>
<TabsTrigger value="profile">Profile Settings</TabsTrigger>
</TabsList>
<TabsContent value="client">
<Card className="w-xl mx-auto">
<CardContent className="flex flex-col gap-3">
<Settings
settings={settings}
setSettings={setSettings}
schema={CLIENT_SCHEMA}
onSave={() => {
setClientSettings(settings);
router.refresh();
}}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="profile">
<Card className="w-xl mx-auto">
<CardContent className="flex flex-col gap-3">
<Settings
settings={profileSettings}
setSettings={setProfileSettings}
schema={PROFILE_SCHEMA}
onSave={async () => {
await axios.post("/api/profile", profileSettings);
}}
/>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
+84 -18
View File
@@ -8,8 +8,8 @@ import {
SettingsIcon, SettingsIcon,
Volume2Icon, Volume2Icon,
} from "lucide-react"; } from "lucide-react";
import { Server, Channel } from "@/types/types"; import { Server, Channel, Message } from "@/types/types";
import { useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { import {
Dialog, Dialog,
@@ -25,32 +25,77 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Card } from "./ui/card"; import { Card } from "./ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { ScrollArea } from "./ui/scroll-area"; import { ScrollArea } from "./ui/scroll-area";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { NumberMap } from "@/types/typeUtils"; import useUser, { User } from "@/hooks/get-user";
import { User } from "@/hooks/get-user";
import ProfilePicture from "./ProfilePicture"; import ProfilePicture from "./ProfilePicture";
import auth, { makeAddress } from "@/lib/auth";
import { toast } from "sonner";
import { StringMap } from "@/types/typeUtils";
import SettingsDialog from "./settings/SettingsDialog";
export default function AppSidebar({ export default function AppSidebar({
user,
server,
children, children,
chatWith,
wsRef,
setMessages,
onNewMessage,
setUser,
server,
}: Readonly<{ }: Readonly<{
server?: Server; wsRef: React.RefObject<WebSocket | null>;
user: User | null; setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
onNewMessage?: (msg: Message) => void;
userList: StringMap<User>;
setUserList: React.Dispatch<React.SetStateAction<StringMap<User>>>;
chatWith?: string;
children?: React.ReactNode; children?: React.ReactNode;
setUser?: React.Dispatch<React.SetStateAction<User | undefined>>;
server?: Server | undefined;
}>) { }>) {
const [servers, setServers] = useState<[string, string][]>([]); const [servers, setServers] = useState<[string, string][]>([]);
const [newServer, setNewServer] = useState({ name: "", ip: "" }); const [newServer, setNewServer] = useState({ name: "", ip: "" });
const router = useRouter(); const router = useRouter();
const user = useUser();
const [userList, setUserList] = useState<StringMap<User>>({});
const ip = "192.168.100.3";
useEffect(() => { useEffect(() => {
const s = Cookies.get("servers")?.split(","); const s = Cookies.get("servers")?.split(",");
if (s) setServers(s.map((x) => x.trim().split("@") as [string, string])); if (s) setServers(s.map((x) => x.trim().split("@") as [string, string]));
}, []); }, []);
useEffect(() => {
if (!ip || !user) return;
if (setUser) setUser(user);
auth(
makeAddress(ip, 7090),
wsRef,
() => {},
setMessages,
(m) => {
if (m.from !== chatWith && m.from !== user.id)
toast(
`New message from ${userList[m.from]?.display_name || m.from}`,
{
description: m.contents.slice(0, 80),
action: {
label: "View",
onClick: () => router.push(`/chat/${m.from}`),
},
}
);
if (onNewMessage) onNewMessage(m);
}
);
setUserList((prev) => {
prev[user.id] = user;
return prev;
});
}, [chatWith, user]);
const handleAddServer = () => { const handleAddServer = () => {
if (!newServer.ip) return; if (!newServer.ip) return;
const updatedServers = [ const updatedServers = [
@@ -151,9 +196,7 @@ export default function AppSidebar({
</Dialog> </Dialog>
<footer className="mt-auto"> <footer className="mt-auto">
<Link href="/settings"> <SettingsDialog />
<SettingsIcon />
</Link>
</footer> </footer>
</div> </div>
@@ -182,16 +225,21 @@ export default function AppSidebar({
</> </>
) : ( ) : (
<> <>
<DMItem name="Alice" status="online" /> <DMItem name="Alice" status="online" avatar="" />
<DMItem name="Bob" status="offline" /> <DMItem name="Bob" status="offline" avatar="" />
<DMItem name="Charlie" status="away" /> <DMItem name="Charlie" status="away" avatar="" />
</> </>
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
<footer className="mt-auto pb-3 pl-3 pr-3"> <footer className="mt-auto pb-3 pl-3 pr-3">
<DMItem name={user?.username || "Loading.."} status="online" /> <DMItem
name={user?.display_name || "Loading.."}
avatar={user?.avatar_url || ""}
status="online"
settings
/>
</footer> </footer>
</div> </div>
</div> </div>
@@ -201,14 +249,32 @@ export default function AppSidebar({
); );
} }
function DMItem({ name, status }: { name: string; status: string }) { function DMItem({
name,
avatar,
status,
settings,
}: {
name: string;
avatar: string;
status: string;
settings?: boolean;
}) {
const router = useRouter();
return ( return (
<Card className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent"> <Card className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent">
<ProfilePicture name={name} /> <ProfilePicture name={name} url={avatar} />
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium">{name}</span> <span className="text-sm font-medium">{name}</span>
<span className="text-xs text-muted-foreground">{status}</span> <span className="text-xs text-muted-foreground">{status}</span>
</div> </div>
{settings && (
<SettingsDialog
className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
tab="profile"
/>
)}
</Card> </Card>
); );
} }
@@ -25,7 +25,7 @@ export type SettingsSchema = StringMap<
"boolean" | "string" | "number" | string[] | RegexSetting "boolean" | "string" | "number" | string[] | RegexSetting
>; >;
export default function Settings<T>({ export default function SettingSchema<T>({
settings, settings,
setSettings, setSettings,
schema, schema,
@@ -70,10 +70,10 @@ export default function Settings<T>({
if (Array.isArray(type)) { if (Array.isArray(type)) {
return ( return (
<Select <Select
value={(settings as any)[key]} value={(settings as any)[key] || []}
onValueChange={(val) => handleChange(key, val)} onValueChange={(val) => handleChange(key, val)}
> >
<SelectTrigger className="w-[200px]"> <SelectTrigger>
<SelectValue placeholder={key} /> <SelectValue placeholder={key} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -88,7 +88,7 @@ export default function Settings<T>({
} else if (type === "boolean") { } else if (type === "boolean") {
return ( return (
<Checkbox <Checkbox
checked={(settings as any)[key]} checked={(settings as any)[key] || false}
onCheckedChange={(val) => handleChange(key, val)} onCheckedChange={(val) => handleChange(key, val)}
/> />
); );
@@ -96,7 +96,7 @@ export default function Settings<T>({
return ( return (
<Input <Input
type="text" type="text"
value={(settings as any)[key]} value={(settings as any)[key] || ""}
onChange={(e) => handleChange(key, e.target.value)} onChange={(e) => handleChange(key, e.target.value)}
/> />
); );
@@ -104,7 +104,7 @@ export default function Settings<T>({
return ( return (
<Input <Input
type="number" type="number"
value={(settings as any)[key]} value={(settings as any)[key] || 0}
onChange={(e) => handleChange(key, Number(e.target.value))} onChange={(e) => handleChange(key, Number(e.target.value))}
/> />
); );
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { useClientSettings } from "@/hooks/use-settings";
import { setClientSettings } from "@/lib/clientSettings";
import SettingSchema from "./SettingSchema";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { CardContent } from "@/components/ui/card";
import {
PROFILE_SCHEMA,
CLIENT_SCHEMA,
ProfileSettings,
} from "@/types/settings";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import axios from "axios";
export default function Settings({
tab,
profileSettings,
setProfileSettings,
}: {
tab?: string;
profileSettings: ProfileSettings;
setProfileSettings: React.Dispatch<React.SetStateAction<ProfileSettings>>;
}) {
const [settings, setSettings] = useClientSettings();
const router = useRouter();
return (
<Tabs className="mx-auto mt-3" defaultValue={tab || "client"}>
<TabsList className="mx-auto">
<TabsTrigger value="client">Client Settings</TabsTrigger>
<TabsTrigger value="profile">Profile Settings</TabsTrigger>
</TabsList>
<TabsContent value="client">
<Card className="mx-auto">
<CardContent className="flex flex-col gap-3">
<SettingSchema
settings={settings}
setSettings={setSettings}
schema={CLIENT_SCHEMA}
onSave={() => {
setClientSettings(settings);
router.refresh();
}}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="profile">
<Card className="mx-auto">
<CardContent className="flex flex-col gap-3">
<SettingSchema
settings={profileSettings}
setSettings={setProfileSettings}
schema={PROFILE_SCHEMA}
onSave={async () => {
await axios.post("/api/profile", profileSettings);
}}
/>
</CardContent>
</Card>
</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,53 @@
import { SettingsIcon } from "lucide-react";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { cn } from "@/lib/utils";
import Settings from "./Settings";
import { ProfileSettings } from "@/types/settings";
import { useProfileSettings } from "@/hooks/use-settings";
export default function SettingsDialog({
className,
tab,
}: {
className?: string;
tab?: string;
profile?: ProfileSettings;
}) {
const [profileSettings, setProfileSettings] = useProfileSettings();
return (
<Dialog>
<DialogTrigger asChild>
<Button
className={cn(
"bg-transparent text-accent-foreground hover:text-accent",
className
)}
>
<SettingsIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<div className="w-full">
<Settings
tab={tab}
profileSettings={profileSettings}
setProfileSettings={setProfileSettings}
/>
</div>
</DialogContent>
</Dialog>
);
}
+1 -4
View File
@@ -14,7 +14,6 @@ export interface User {
export default function useUser() { export default function useUser() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
@@ -31,13 +30,11 @@ export default function useUser() {
setUser(res); setUser(res);
} catch { } catch {
router.push("/login"); router.push("/login");
} finally {
setLoading(false);
} }
} }
fetchUser(); fetchUser();
}, [router]); }, [router]);
return { user, loading }; return user;
} }
+2 -1
View File
@@ -13,6 +13,7 @@ export default async function auth(
setMessages: Dispatch<SetStateAction<Message[]>>, setMessages: Dispatch<SetStateAction<Message[]>>,
onNewMessage?: (m: Message) => void onNewMessage?: (m: Message) => void
) { ) {
console.log("Authenticating with server at:", ip);
const server_auth = ( const server_auth = (
(await axios.post("/api/auth", { server_ip: ip })).data as any (await axios.post("/api/auth", { server_ip: ip })).data as any
).token; ).token;
@@ -37,7 +38,7 @@ export default async function auth(
console.log("Message received:", data); console.log("Message received:", data);
if (data.version) { if (data.version) {
setServer(data); setServer({ channels: [], ...data });
return; return;
} }
+4 -1
View File
@@ -1,4 +1,7 @@
import { RegexSetting, SettingsSchema } from "@/app/settings/settings"; import {
RegexSetting,
SettingsSchema,
} from "@/components/settings/SettingSchema";
export interface ClientSettings { export interface ClientSettings {
theme: "light" | "dark"; theme: "light" | "dark";
+2 -2
View File
@@ -6,8 +6,8 @@ export interface Channel {
export interface Message { export interface Message {
id: number; id: number;
channel_id: string | number; channel_id: string;
from: number; from: string;
contents: string; contents: string;
timestamp: number; timestamp: number;
} }