+10
-33
@@ -1,49 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import MessageBox from "@/components/MessageBox";
|
import MessageBox from "@/components/MessageBox";
|
||||||
import AppSidebar from "@/components/Sidebar";
|
import AppSidebar from "@/components/Sidebar";
|
||||||
import { Message } from "@/types/types";
|
|
||||||
import { UserProfile } from "@/hooks/get-user";
|
|
||||||
import { StringMap } from "@/types/typeUtils";
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useMessages } from "@/hooks/use-messages";
|
import useApp from "@/hooks/use-app";
|
||||||
|
|
||||||
export default function DMs() {
|
export default function DMs() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const app = useApp();
|
||||||
const messages = useMessages((s) => s.messages);
|
|
||||||
const addMessage = useMessages((s) => s.addMessage);
|
|
||||||
const [userList, setUserList] = useState<StringMap<UserProfile>>({});
|
|
||||||
const [user, setUser] = useState<UserProfile | undefined>();
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar
|
<AppSidebar app={app}>
|
||||||
open={sidebarOpen}
|
|
||||||
setOpen={setSidebarOpen}
|
|
||||||
addMessage={addMessage}
|
|
||||||
wsRef={wsRef}
|
|
||||||
userList={userList}
|
|
||||||
setUserList={setUserList}
|
|
||||||
chatWith={id}
|
|
||||||
setUser={setUser}
|
|
||||||
messages={messages}
|
|
||||||
>
|
|
||||||
<MessageBox
|
<MessageBox
|
||||||
toggleSidebar={() => setSidebarOpen(true)}
|
app={app}
|
||||||
channelName={userList[id]?.display_name || id}
|
channelName={app.profiles[id]?.display_name || id}
|
||||||
userList={userList}
|
messages={app.messages.filter(
|
||||||
setUserList={setUserList}
|
|
||||||
messages={messages
|
|
||||||
.filter(
|
|
||||||
(m) =>
|
(m) =>
|
||||||
(m.channel_id === id && m.from === user?.id) ||
|
(m.channel_id === id && m.from === app.profile?.id) ||
|
||||||
(m.from === id && m.channel_id === user?.id)
|
(m.from === id && m.channel_id === app.profile?.id)
|
||||||
)
|
)}
|
||||||
.toReversed()}
|
|
||||||
sendMessage={(message) => {
|
sendMessage={(message) => {
|
||||||
wsRef.current?.send(
|
app.node.current?.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "send_message",
|
type: "send_message",
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
+4
-43
@@ -1,59 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import AppSidebar from "@/components/Sidebar";
|
import AppSidebar from "@/components/Sidebar";
|
||||||
import { UserProfile } from "@/hooks/get-user";
|
|
||||||
import { StringMap } from "@/types/typeUtils";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import ProfilePicture from "@/components/ProfilePicture";
|
import useApp from "@/hooks/use-app";
|
||||||
import { Message } from "@/types/types";
|
|
||||||
import { useMessages } from "@/hooks/use-messages";
|
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const app = useApp();
|
||||||
const [userList, setUserList] = useState<StringMap<UserProfile>>({});
|
|
||||||
const messages = useMessages((s) => s.messages);
|
|
||||||
const addMessage = useMessages((s) => s.addMessage);
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar
|
<AppSidebar app={app}>
|
||||||
open={sidebarOpen}
|
|
||||||
setOpen={setSidebarOpen}
|
|
||||||
wsRef={wsRef}
|
|
||||||
userList={userList}
|
|
||||||
setUserList={setUserList}
|
|
||||||
addMessage={addMessage}
|
|
||||||
messages={messages}
|
|
||||||
>
|
|
||||||
<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></CardContent>
|
||||||
{/* <DMItem name="Alice" status="online" avatar="" /> */}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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} 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>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,50 +1,47 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
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 { Server as ServerType } from "@/types/types";
|
||||||
import { UserProfile } from "@/hooks/get-user";
|
|
||||||
import { StringMap } from "@/types/typeUtils";
|
|
||||||
import auth from "@/lib/auth";
|
import auth from "@/lib/auth";
|
||||||
import { useMessages } from "@/hooks/use-messages";
|
import { useServerMessages } from "@/hooks/use-messages";
|
||||||
|
import useApp from "@/hooks/use-app";
|
||||||
|
import { useEffectOnceWhenReady } from "@/hooks/use-once";
|
||||||
|
|
||||||
export default function Server() {
|
export default function Server() {
|
||||||
const { ip } = useParams<{ ip: string }>();
|
const { ip } = useParams<{ ip: string }>();
|
||||||
const wsServerRef = useRef<WebSocket | null>(null);
|
const serverRef = useRef<WebSocket | null>(null);
|
||||||
const wsNodeRef = useRef<WebSocket | null>(null);
|
|
||||||
const [server, setServer] = useState<undefined | ServerType>();
|
const [server, setServer] = useState<undefined | ServerType>();
|
||||||
const [userList, setUserList] = useState<StringMap<UserProfile>>({});
|
const app = useApp();
|
||||||
const messages = useMessages((s) => s.messages);
|
const { messages, addMessage } = useServerMessages();
|
||||||
const addMessage = useMessages((s) => s.addMessage);
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffectOnceWhenReady(
|
||||||
|
() => {
|
||||||
if (!ip) return;
|
if (!ip) return;
|
||||||
auth(ip, wsServerRef, setServer, addMessage);
|
const msgs = messages[ip];
|
||||||
}, [ip]);
|
auth(
|
||||||
|
ip,
|
||||||
|
serverRef,
|
||||||
|
setServer,
|
||||||
|
(m) => addMessage(ip, m),
|
||||||
|
() => {},
|
||||||
|
msgs ? msgs[msgs.length - 1]?.id : undefined
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[ip, messages],
|
||||||
|
[undefined, (v) => v[ip]]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar
|
<AppSidebar app={app} server={server}>
|
||||||
open={sidebarOpen}
|
|
||||||
setOpen={setSidebarOpen}
|
|
||||||
wsRef={wsNodeRef}
|
|
||||||
addMessage={() => {}}
|
|
||||||
userList={userList}
|
|
||||||
setUserList={setUserList}
|
|
||||||
server={server}
|
|
||||||
>
|
|
||||||
<MessageBox
|
<MessageBox
|
||||||
toggleSidebar={() => setSidebarOpen(true)}
|
app={app}
|
||||||
channelName="General"
|
channelName="General"
|
||||||
userList={userList}
|
messages={messages[ip]?.filter((m) => m.channel_id === "general") || []}
|
||||||
setUserList={setUserList}
|
|
||||||
messages={messages
|
|
||||||
.filter((m) => m.channel_id === "general")
|
|
||||||
.toReversed()}
|
|
||||||
sendMessage={(message) => {
|
sendMessage={(message) => {
|
||||||
wsServerRef.current?.send(
|
serverRef.current?.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "send_message",
|
type: "send_message",
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -10,53 +10,45 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "./ui/dropdown-menu";
|
} from "./ui/dropdown-menu";
|
||||||
import { ChevronLeft, ChevronLeftIcon, SmilePlusIcon } from "lucide-react";
|
import { ChevronLeftIcon, SmilePlusIcon } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { format, isToday, isYesterday } from "date-fns";
|
import { format, isToday, isYesterday } from "date-fns";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { NumberMap, StringMap } from "@/types/typeUtils";
|
|
||||||
import { UserProfile } from "@/hooks/get-user";
|
import { UserProfile } from "@/hooks/get-user";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ProfilePicture from "./ProfilePicture";
|
import ProfilePicture from "./ProfilePicture";
|
||||||
|
import App from "@/types/app";
|
||||||
|
|
||||||
function MessageContainer({
|
function MessageContainer({ message, app }: { message: Message; app: App }) {
|
||||||
message,
|
|
||||||
userList,
|
|
||||||
setUserList,
|
|
||||||
}: {
|
|
||||||
message: Message;
|
|
||||||
userList: StringMap<UserProfile>;
|
|
||||||
setUserList: React.Dispatch<React.SetStateAction<NumberMap<UserProfile>>>;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios
|
axios
|
||||||
.get("/api/profile/", { params: { id: message.from } })
|
.get("/api/profile/", { params: { id: message.from } })
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
setUserList((prev) => ({
|
app.setProfiles((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[message.from]: res.data as UserProfile,
|
[message.from]: res.data as UserProfile,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [message.from, setUserList]);
|
}, [message.from]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 rounded-lg p-4 hover:bg-accent">
|
<div className="flex flex-col gap-3 rounded-lg p-4 hover:bg-accent">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* <div className="w-8 h-8 rounded-full bg-gray-400" /> */}
|
{/* <div className="w-8 h-8 rounded-full bg-gray-400" /> */}
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
url={userList[message.from]?.avatar_url}
|
url={app.profiles[message.from]?.avatar_url}
|
||||||
name={String(
|
name={String(
|
||||||
userList[message.from]
|
app.profiles[message.from]
|
||||||
? userList[message.from].display_name
|
? app.profiles[message.from].display_name
|
||||||
: message.from
|
: message.from
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<span className="font-bold flex gap-1 items-center">
|
<span className="font-bold flex gap-1 items-center">
|
||||||
{userList[message.from]
|
{app.profiles[message.from]
|
||||||
? userList[message.from].display_name
|
? app.profiles[message.from].display_name
|
||||||
: message.from}
|
: message.from}
|
||||||
|
|
||||||
<p className="text-neutral-500 text-xs">
|
<p className="text-neutral-500 text-xs">
|
||||||
@@ -139,43 +131,37 @@ function MessageContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageBox({
|
export default function MessageBox({
|
||||||
messages,
|
app,
|
||||||
userList,
|
|
||||||
setUserList,
|
|
||||||
sendMessage,
|
sendMessage,
|
||||||
channelName,
|
channelName,
|
||||||
toggleSidebar,
|
messages,
|
||||||
}: {
|
}: {
|
||||||
messages: Message[];
|
app: App;
|
||||||
userList: NumberMap<UserProfile>;
|
|
||||||
setUserList: React.Dispatch<React.SetStateAction<NumberMap<UserProfile>>>;
|
|
||||||
sendMessage: (m: string) => void;
|
sendMessage: (m: string) => void;
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
toggleSidebar: () => void;
|
messages: Message[];
|
||||||
}) {
|
}) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-col pb-5 pl-5 gap-5">
|
<div className="h-svh w-full max-h-svh flex flex-col pb-5 pl-5 gap-5">
|
||||||
{channelName && (
|
{channelName && (
|
||||||
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
|
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
|
||||||
<span onClick={toggleSidebar} className="cursor-pointer flex">
|
<span
|
||||||
|
onClick={() => app.setSidebarOpen(true)}
|
||||||
|
className="cursor-pointer flex"
|
||||||
|
>
|
||||||
<ChevronLeftIcon className="w-5 h-5" />
|
<ChevronLeftIcon className="w-5 h-5" />
|
||||||
{channelName}
|
{channelName}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<div className="w-full h-max flex-1 overflow-y-scroll flex gap-2 flex-col-reverse pr-5">
|
<div className="flex flex-col-reverse overflow-y-scroll">
|
||||||
{messages.map((msg) => (
|
{messages.toReversed().map((msg) => (
|
||||||
<MessageContainer
|
<MessageContainer key={msg.id} message={msg} app={app} />
|
||||||
key={msg.id}
|
|
||||||
message={msg}
|
|
||||||
userList={userList}
|
|
||||||
setUserList={setUserList}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<footer className="w-full flex gap-3 pr-5 mt-auto">
|
<footer className="flex gap-3 pr-5 w-full mt-auto">
|
||||||
<Input
|
<Input
|
||||||
className=""
|
className=""
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
|
|||||||
+28
-41
@@ -26,53 +26,39 @@ import { Button } from "./ui/button";
|
|||||||
import { Card } from "./ui/card";
|
import { Card } from "./ui/card";
|
||||||
import { ScrollArea } from "./ui/scroll-area";
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import useUser, { UserProfile } from "@/hooks/get-user";
|
import { UserProfile } from "@/hooks/get-user";
|
||||||
import ProfilePicture from "./ProfilePicture";
|
import ProfilePicture from "./ProfilePicture";
|
||||||
import auth from "@/lib/auth";
|
import auth from "@/lib/auth";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { StringMap } from "@/types/typeUtils";
|
import { StringMap } from "@/types/typeUtils";
|
||||||
import SettingsDialog from "./settings/SettingsDialog";
|
import SettingsDialog from "./settings/SettingsDialog";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import axios from "axios";
|
|
||||||
import { get, Response } from "@/lib/request";
|
import { get, Response } from "@/lib/request";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import App from "@/types/app";
|
||||||
|
|
||||||
export default function AppSidebar({
|
export default function AppSidebar({
|
||||||
children,
|
children,
|
||||||
chatWith,
|
chatWith,
|
||||||
wsRef,
|
|
||||||
onNewMessage,
|
onNewMessage,
|
||||||
setUser,
|
|
||||||
server,
|
server,
|
||||||
messages,
|
app,
|
||||||
addMessage,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
wsRef: React.RefObject<WebSocket | null>;
|
|
||||||
addMessage: (msg: Message) => void;
|
|
||||||
messages?: Message[];
|
|
||||||
onNewMessage?: (msg: Message) => void;
|
onNewMessage?: (msg: Message) => void;
|
||||||
userList: StringMap<UserProfile>;
|
|
||||||
setUserList: React.Dispatch<React.SetStateAction<StringMap<UserProfile>>>;
|
|
||||||
chatWith?: string;
|
chatWith?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
setUser?: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
|
|
||||||
server?: Server | undefined;
|
server?: Server | undefined;
|
||||||
open: boolean;
|
app: App;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}>) {
|
}>) {
|
||||||
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<UserProfile>>({});
|
|
||||||
|
|
||||||
const dms = Array.from(
|
const dms = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
messages
|
app.messages
|
||||||
?.flatMap((item) => [item.from, item.channel_id])
|
?.flatMap((item) => [item.from, item.channel_id])
|
||||||
.filter((item) => item !== user?.id) || []
|
.filter((item) => item !== app.profile?.id) || []
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,17 +68,16 @@ export default function AppSidebar({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.node_address) return;
|
if (!app.profile?.node_address) return;
|
||||||
if (setUser) setUser(user);
|
|
||||||
auth(
|
auth(
|
||||||
user?.node_address,
|
app.profile?.node_address,
|
||||||
wsRef,
|
app.node,
|
||||||
() => {},
|
() => {},
|
||||||
addMessage,
|
app.addMessage,
|
||||||
(m) => {
|
(m) => {
|
||||||
if (m.from !== chatWith && m.from !== user.id)
|
if (m.from !== chatWith && m.from !== app.profile?.id)
|
||||||
toast(
|
toast(
|
||||||
`New message from ${userList[m.from]?.display_name || m.from}`,
|
`New message from ${app.profiles[m.from]?.display_name || m.from}`,
|
||||||
{
|
{
|
||||||
description: m.contents.slice(0, 80),
|
description: m.contents.slice(0, 80),
|
||||||
action: {
|
action: {
|
||||||
@@ -105,13 +90,13 @@ export default function AppSidebar({
|
|||||||
|
|
||||||
if (onNewMessage) onNewMessage(m);
|
if (onNewMessage) onNewMessage(m);
|
||||||
},
|
},
|
||||||
messages ? messages[messages.length - 1]?.id : undefined
|
app.messages ? app.messages[app.messages.length - 1]?.id : undefined
|
||||||
);
|
);
|
||||||
setUserList((prev) => {
|
app.setProfiles((prev) => {
|
||||||
prev[user.id] = user;
|
if (app.profile) prev[app.profile.id] = app.profile;
|
||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
}, [chatWith, user]);
|
}, [chatWith, app.profile]);
|
||||||
|
|
||||||
const handleAddServer = () => {
|
const handleAddServer = () => {
|
||||||
if (!newServer.ip) return;
|
if (!newServer.ip) return;
|
||||||
@@ -132,7 +117,7 @@ export default function AppSidebar({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-r transition-all duration-300 ease-in-out overflow-hidden flex",
|
"border-r transition-all duration-300 ease-in-out overflow-hidden flex",
|
||||||
open ? "w-5xl md:w-md" : "w-0 md:w-md"
|
app.sidebarOpen ? "w-5xl md:w-md" : "w-0 md:w-md"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-16 flex flex-col items-center gap-4 py-4 bg-muted border-r">
|
<div className="w-16 flex flex-col items-center gap-4 py-4 bg-muted border-r">
|
||||||
@@ -250,12 +235,14 @@ export default function AppSidebar({
|
|||||||
{dms.map((id) => (
|
{dms.map((id) => (
|
||||||
<DMItem
|
<DMItem
|
||||||
name={
|
name={
|
||||||
getUser(id, userList, setUserList)?.display_name || id
|
getUser(id, app.profiles, app.setProfiles)
|
||||||
|
?.display_name || id
|
||||||
}
|
}
|
||||||
id={id}
|
id={id}
|
||||||
status="online"
|
status="online"
|
||||||
avatar={
|
avatar={
|
||||||
getUser(id, userList, setUserList)?.avatar_url || ""
|
getUser(id, app.profiles, app.setProfiles)
|
||||||
|
?.avatar_url || ""
|
||||||
}
|
}
|
||||||
key={id}
|
key={id}
|
||||||
/>
|
/>
|
||||||
@@ -267,21 +254,21 @@ export default function AppSidebar({
|
|||||||
|
|
||||||
<footer className="mt-auto pb-3 pl-3 pr-3">
|
<footer className="mt-auto pb-3 pl-3 pr-3">
|
||||||
<DMItem
|
<DMItem
|
||||||
name={user?.display_name || "Loading.."}
|
name={app.profile?.display_name || "Loading.."}
|
||||||
id={user?.id || "me"}
|
id={app.profile?.id || "me"}
|
||||||
avatar={user?.avatar_url || ""}
|
avatar={app.profile?.avatar_url || ""}
|
||||||
status="online"
|
status="online"
|
||||||
settings
|
settings
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{open ? (
|
{app.sidebarOpen ? (
|
||||||
<div
|
<div
|
||||||
className="w-screen h-full overflow-x-hidden"
|
className="w-full h-full overflow-x-hidden"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => app.setSidebarOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="overflow-x-hidden w-screen min-w-max flex-shrink-0">
|
<div className="overflow-x-hidden min-w-max flex-shrink-0">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import App from "@/types/app";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useMessages } from "./use-messages";
|
||||||
|
import { StringMap } from "@/types/typeUtils";
|
||||||
|
import useUser, { UserProfile } from "./get-user";
|
||||||
|
|
||||||
|
export default function useApp(): App {
|
||||||
|
const node = useRef<WebSocket | null>(null);
|
||||||
|
const [profiles, setProfiles] = useState<StringMap<UserProfile>>({});
|
||||||
|
const { messages, addMessage } = useMessages();
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
const profile = useUser();
|
||||||
|
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
profile,
|
||||||
|
profiles,
|
||||||
|
setProfiles,
|
||||||
|
messages,
|
||||||
|
addMessage,
|
||||||
|
sidebarOpen,
|
||||||
|
setSidebarOpen,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,13 @@ interface MessageStore {
|
|||||||
clearMessages: () => void;
|
clearMessages: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerMessageStore = {
|
||||||
|
messages: Record<string, Message[]>; // server_id → messages
|
||||||
|
setMessages: (serverId: string, msgs: Message[]) => void;
|
||||||
|
addMessage: (serverId: string, msg: Message) => void;
|
||||||
|
clearMessages: (serverId?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const useMessages = create<MessageStore>()(
|
export const useMessages = create<MessageStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
@@ -25,3 +32,44 @@ export const useMessages = create<MessageStore>()(
|
|||||||
{ name: "chat-messages" }
|
{ name: "chat-messages" }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useServerMessages = create<ServerMessageStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
messages: {},
|
||||||
|
|
||||||
|
setMessages: (serverId, msgs) =>
|
||||||
|
set((state) => ({
|
||||||
|
messages: { ...state.messages, [serverId]: msgs },
|
||||||
|
})),
|
||||||
|
|
||||||
|
addMessage: (serverId, msg) =>
|
||||||
|
set((state) => {
|
||||||
|
const serverMsgs = state.messages[serverId] || [];
|
||||||
|
|
||||||
|
// avoid duplicates
|
||||||
|
if (serverMsgs.some((m) => m.id === msg.id)) return state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: {
|
||||||
|
...state.messages,
|
||||||
|
[serverId]: [...serverMsgs, msg],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearMessages: (serverId) =>
|
||||||
|
set((state) =>
|
||||||
|
serverId
|
||||||
|
? {
|
||||||
|
messages: {
|
||||||
|
...state.messages,
|
||||||
|
[serverId]: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { messages: {} }
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{ name: "server-messages" }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function useEffectOnceWhenReady(
|
||||||
|
effect: () => void,
|
||||||
|
deps: any[],
|
||||||
|
testers: (undefined | ((m: any) => boolean))[]
|
||||||
|
) {
|
||||||
|
const hasRun = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allReady = deps.every(
|
||||||
|
(d, i) => d !== undefined && d !== null && (!testers[i] || testers[i](d))
|
||||||
|
);
|
||||||
|
if (!allReady || hasRun.current) return;
|
||||||
|
hasRun.current = true;
|
||||||
|
effect();
|
||||||
|
}, deps);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Dispatch, RefObject, SetStateAction } from "react";
|
||||||
|
import { StringMap } from "./typeUtils";
|
||||||
|
import { UserProfile } from "@/hooks/get-user";
|
||||||
|
import { Message } from "./types";
|
||||||
|
|
||||||
|
export type SetState<T> = Dispatch<SetStateAction<T>>;
|
||||||
|
|
||||||
|
export default interface App {
|
||||||
|
node: RefObject<WebSocket | null>;
|
||||||
|
profile: UserProfile | null;
|
||||||
|
|
||||||
|
profiles: StringMap<UserProfile>;
|
||||||
|
setProfiles: SetState<StringMap<UserProfile>>;
|
||||||
|
|
||||||
|
messages: Message[];
|
||||||
|
addMessage: (v: Message) => void;
|
||||||
|
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
setSidebarOpen: SetState<boolean>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user