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 { Message } from "@/types/types";
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 { useParams } from "next/navigation";
@@ -14,36 +14,37 @@ import { useParams } from "next/navigation";
export default function DMs() {
const { id } = useParams<{ id: string }>();
const numericId = id ? Number(id) : undefined;
const wsRef = useRef<WebSocket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const { user, loading } = useUser();
const [userList, setUserList] = useState<NumberMap<User>>({});
useEffect(() => {
if (!numericId || !user || loading) return;
auth(makeAddress("localhost", 7090), wsRef, () => {}, setMessages);
setUserList((prev) => {
prev[user.id] = user;
return prev;
});
}, [id, user, loading]);
const [userList, setUserList] = useState<StringMap<User>>({});
const [user, setUser] = useState<User | undefined>();
return (
<AppSidebar user={user}>
<AppSidebar
setMessages={setMessages}
wsRef={wsRef}
userList={userList}
setUserList={setUserList}
chatWith={id}
setUser={setUser}
>
<MessageBox
channelName="Alice"
channelName={userList[id]?.display_name || id}
userList={userList}
setUserList={setUserList}
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()}
sendMessage={(message) => {
wsRef.current?.send(
JSON.stringify({
type: "send_message",
params: {
channel_id: numericId,
channel_id: id,
contents: message,
},
})
+21 -34
View File
@@ -1,60 +1,47 @@
"use client";
import { useEffect, useRef, useState } from "react";
import MessageBox from "@/components/MessageBox";
import { useRef, useState } from "react";
import AppSidebar from "@/components/Sidebar";
import { Message } from "@/types/types";
import useUser, { User } from "@/hooks/get-user";
import { NumberMap } from "@/types/typeUtils";
import auth, { makeAddress } from "@/lib/auth";
import { User } from "@/hooks/get-user";
import { StringMap } from "@/types/typeUtils";
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";
export default function Chat() {
const ip = "localhost";
const wsRef = useRef<WebSocket | null>(null);
const { user, loading } = useUser();
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]);
const [userList, setUserList] = useState<StringMap<User>>({});
return (
<AppSidebar user={{ id: 173, username: "leo", email: "" }}>
<AppSidebar
wsRef={wsRef}
userList={userList}
setUserList={setUserList}
setMessages={() => {}}
>
<Card className="bg-background w-full">
<CardHeader>
<CardTitle>Direct Messages</CardTitle>
</CardHeader>
<CardContent>
<DMItem name="Alice" status="online" />
<DMItem name="Alice" status="online" avatar="" />
</CardContent>
</Card>
</AppSidebar>
);
}
function DMItem({ name, status }: { name: string; status: string }) {
function DMItem({
name,
avatar,
status,
}: {
name: string;
avatar: string;
status: string;
}) {
return (
<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">
<span className="text-sm font-medium">{name}</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 AppSidebar from "@/components/Sidebar";
import { Message, Server as ServerType } from "@/types/types";
import useUser, { User } from "@/hooks/get-user";
import { NumberMap } from "@/types/typeUtils";
import { User } from "@/hooks/get-user";
import { StringMap } from "@/types/typeUtils";
import auth, { makeAddress } from "@/lib/auth";
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 wsRef = useRef<WebSocket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const { user, loading } = useUser();
const [userList, setUserList] = useState<NumberMap<User>>({});
const [userList, setUserList] = useState<StringMap<User>>({});
useEffect(() => {
if (!ip || !user || loading) return;
auth(makeAddress(ip), wsRef, setServer, setMessages);
setUserList((prev) => {
prev[user.id] = user;
return prev;
});
}, [ip, user, loading]);
if (!ip) return;
auth(makeAddress(ip, 7080), wsServerRef, setServer, setMessages);
}, [ip]);
return (
<AppSidebar
user={user}
server={
{
channels: [{ id: "general", name: "General", kind: "text" }],
...server,
} as ServerType
}
wsRef={wsNodeRef}
setMessages={setMessages}
userList={userList}
setUserList={setUserList}
server={server}
>
<MessageBox
channelName="General"
@@ -44,7 +38,7 @@ export default function Server() {
.filter((m) => m.channel_id === "general")
.toReversed()}
sendMessage={(message) => {
wsRef.current?.send(
wsServerRef.current?.send(
JSON.stringify({
type: "send_message",
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,
Volume2Icon,
} from "lucide-react";
import { Server, Channel } from "@/types/types";
import { useEffect, useState } from "react";
import { Server, Channel, Message } from "@/types/types";
import React, { useEffect, useRef, useState } from "react";
import Cookies from "js-cookie";
import {
Dialog,
@@ -25,32 +25,77 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Button } from "./ui/button";
import { Card } from "./ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { ScrollArea } from "./ui/scroll-area";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { NumberMap } from "@/types/typeUtils";
import { User } from "@/hooks/get-user";
import useUser, { User } from "@/hooks/get-user";
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({
user,
server,
children,
chatWith,
wsRef,
setMessages,
onNewMessage,
setUser,
server,
}: Readonly<{
server?: Server;
user: User | null;
wsRef: React.RefObject<WebSocket | 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;
setUser?: React.Dispatch<React.SetStateAction<User | undefined>>;
server?: Server | undefined;
}>) {
const [servers, setServers] = useState<[string, string][]>([]);
const [newServer, setNewServer] = useState({ name: "", ip: "" });
const router = useRouter();
const user = useUser();
const [userList, setUserList] = useState<StringMap<User>>({});
const ip = "192.168.100.3";
useEffect(() => {
const s = Cookies.get("servers")?.split(",");
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 = () => {
if (!newServer.ip) return;
const updatedServers = [
@@ -151,9 +196,7 @@ export default function AppSidebar({
</Dialog>
<footer className="mt-auto">
<Link href="/settings">
<SettingsIcon />
</Link>
<SettingsDialog />
</footer>
</div>
@@ -182,16 +225,21 @@ export default function AppSidebar({
</>
) : (
<>
<DMItem name="Alice" status="online" />
<DMItem name="Bob" status="offline" />
<DMItem name="Charlie" status="away" />
<DMItem name="Alice" status="online" avatar="" />
<DMItem name="Bob" status="offline" avatar="" />
<DMItem name="Charlie" status="away" avatar="" />
</>
)}
</div>
</ScrollArea>
<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>
</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 (
<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">
<span className="text-sm font-medium">{name}</span>
<span className="text-xs text-muted-foreground">{status}</span>
</div>
{settings && (
<SettingsDialog
className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
tab="profile"
/>
)}
</Card>
);
}
@@ -25,7 +25,7 @@ export type SettingsSchema = StringMap<
"boolean" | "string" | "number" | string[] | RegexSetting
>;
export default function Settings<T>({
export default function SettingSchema<T>({
settings,
setSettings,
schema,
@@ -70,10 +70,10 @@ export default function Settings<T>({
if (Array.isArray(type)) {
return (
<Select
value={(settings as any)[key]}
value={(settings as any)[key] || []}
onValueChange={(val) => handleChange(key, val)}
>
<SelectTrigger className="w-[200px]">
<SelectTrigger>
<SelectValue placeholder={key} />
</SelectTrigger>
<SelectContent>
@@ -88,7 +88,7 @@ export default function Settings<T>({
} else if (type === "boolean") {
return (
<Checkbox
checked={(settings as any)[key]}
checked={(settings as any)[key] || false}
onCheckedChange={(val) => handleChange(key, val)}
/>
);
@@ -96,7 +96,7 @@ export default function Settings<T>({
return (
<Input
type="text"
value={(settings as any)[key]}
value={(settings as any)[key] || ""}
onChange={(e) => handleChange(key, e.target.value)}
/>
);
@@ -104,7 +104,7 @@ export default function Settings<T>({
return (
<Input
type="number"
value={(settings as any)[key]}
value={(settings as any)[key] || 0}
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() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
@@ -31,13 +30,11 @@ export default function useUser() {
setUser(res);
} catch {
router.push("/login");
} finally {
setLoading(false);
}
}
fetchUser();
}, [router]);
return { user, loading };
return user;
}
+2 -1
View File
@@ -13,6 +13,7 @@ export default async function auth(
setMessages: Dispatch<SetStateAction<Message[]>>,
onNewMessage?: (m: Message) => void
) {
console.log("Authenticating with server at:", ip);
const server_auth = (
(await axios.post("/api/auth", { server_ip: ip })).data as any
).token;
@@ -37,7 +38,7 @@ export default async function auth(
console.log("Message received:", data);
if (data.version) {
setServer(data);
setServer({ channels: [], ...data });
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 {
theme: "light" | "dark";
+2 -2
View File
@@ -6,8 +6,8 @@ export interface Channel {
export interface Message {
id: number;
channel_id: string | number;
from: number;
channel_id: string;
from: string;
contents: string;
timestamp: number;
}