sync: phase 1 backend + frontend from server
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useConversationStore, type ConversationMessage } from "../stores/conversation.ts";
|
||||
import { useAuthStore } from "../stores/auth.ts";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function renderDMContent(content: string) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ ...props }) => <a {...props} className="text-gb-aqua hover:underline" target="_blank" rel="noreferrer" />,
|
||||
code: ({ ...props }) => <code {...props} className="bg-gb-bg-t px-1 rounded text-gb-fg-s" />,
|
||||
pre: ({ ...props }) => <pre {...props} className="bg-gb-bg-s p-2 my-1 overflow-x-auto text-gb-fg-s" />,
|
||||
blockquote: ({ ...props }) => <blockquote {...props} className="border-l-2 border-gb-orange pl-2 my-1 text-gb-fg-s" />,
|
||||
p: ({ ...props }) => <span {...props} className="inline" />,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function DMChat() {
|
||||
const { conversationId } = useParams<{ conversationId: string }>();
|
||||
const activeId = useConversationStore((s) => s.activeConversationId);
|
||||
const setActive = useConversationStore((s) => s.setActiveConversation);
|
||||
const conversations = useConversationStore((s) => s.conversations);
|
||||
const messagesByConv = useConversationStore((s) => s.messagesByConversation);
|
||||
const fetchMessages = useConversationStore((s) => s.fetchMessages);
|
||||
const sendMessage = useConversationStore((s) => s.sendMessage);
|
||||
const fetchConversations = useConversationStore((s) => s.fetchConversations);
|
||||
const currentUser = useAuthStore((s) => s.user);
|
||||
const [input, setInput] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const id = conversationId || activeId;
|
||||
const conversation = conversations.find((c) => c.id === id);
|
||||
const messages = id ? messagesByConv[id] || [] : [];
|
||||
|
||||
useEffect(() => {
|
||||
fetchConversations();
|
||||
}, [fetchConversations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setActive(id);
|
||||
fetchMessages(id);
|
||||
setError(null);
|
||||
}
|
||||
}, [id, setActive, fetchMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
||||
}, [messages]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!id || !input.trim()) return;
|
||||
setError(null);
|
||||
try {
|
||||
await sendMessage(id, input.trim());
|
||||
setInput("");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to send");
|
||||
}
|
||||
};
|
||||
|
||||
const title = conversation
|
||||
? conversation.type === "group_dm"
|
||||
? conversation.name || conversation.members.map((m) => m.username).join(", ")
|
||||
: conversation.members.find((m) => m.id !== currentUser?.id)?.username || "DM"
|
||||
: id
|
||||
? "[LOADING...]"
|
||||
: "[NO CONVERSATION]";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-gb-bg">
|
||||
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s">
|
||||
@ {title}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-1 font-mono text-sm">
|
||||
{!id && <p className="text-gb-fg-f">[select a conversation]</p>}
|
||||
{id && messages.length === 0 && <p className="text-gb-fg-f">[no messages]</p>}
|
||||
{messages.map((msg: ConversationMessage) => (
|
||||
<div key={msg.id} className="break-words">
|
||||
<span className="text-gb-fg-f">[{formatTime(msg.created_at)}]</span>{" "}
|
||||
<span className="text-gb-aqua"><{msg.author_username}></span>{" "}
|
||||
<span className="text-gb-fg">{renderDMContent(msg.content)}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
{error && (
|
||||
<div className="px-3 pb-1">
|
||||
<p className="text-gb-red text-xs font-mono">ERR: {error}</p>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2">
|
||||
<span className="text-gb-fg-f select-none shrink-0">{">"}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="type a message..."
|
||||
className="terminal-input w-full"
|
||||
disabled={!id}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user