From 3ac25770f0f5d5e39ebd773d583b500f8202e78c Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Tue, 24 Feb 2026 06:50:13 +0100 Subject: [PATCH] feat: Implement WhatsApp-style chat UI hooked to backend --- app/chat/page.tsx | 724 +++++++++++++++++----------------------------- 1 file changed, 266 insertions(+), 458 deletions(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 55227ee..bf34c8d 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -9,16 +9,9 @@ import { type PresenceStatus, } from "@/components/presence-indicator"; import ConnectWallet from "@/components/wallet-connector"; +import { RoomMembersDialog } from "@/components/room-members-dialog"; import { cn } from "@/lib/utils"; -import { useEffect, useMemo, useState } from "react" -import Image from "next/image" -import { Header } from "@/components/header" -import { Footer } from "@/components/footer" -import { PresenceIndicator, type PresenceStatus } from "@/components/presence-indicator" -import ConnectWallet from "@/components/wallet-connector" -import { RoomMembersDialog } from "@/components/room-members-dialog" -import { cn } from "@/lib/utils" -import { getPublicKey, onDisconnect } from "@/app/stellar-wallet-kit" +import { getPublicKey, onDisconnect } from "@/app/stellar-wallet-kit"; import { Search, MessageCircle, @@ -33,11 +26,11 @@ import { MoreVertical, Paperclip, Smile, -} from "lucide-react"; Star, -} from "lucide-react" -import { calculateReputation, trackActivity } from "@/lib/reputation" -import { CONFIG } from "@/lib/config" +} from "lucide-react"; +import { calculateReputation, trackActivity } from "@/lib/reputation"; +import { CONFIG } from "@/lib/config"; +import { createClient } from "@/lib/supabase/client"; type ChatPreview = { id: string; @@ -62,205 +55,154 @@ type ChatMessage = { export default function ChatPage() { const [selectedChatId, setSelectedChatId] = useState(null); const [query, setQuery] = useState(""); - const [messageText, setMessageText] = useState(""); + const [inputMessage, setInputMessage] = useState(""); + const [roomMembersOpen, setRoomMembersOpen] = useState(false); + // Wallet const [walletConnected, setWalletConnected] = useState(false); + const [currentPublicKey, setCurrentPublicKey] = useState(null); + const [reputationScore, setReputationScore] = useState(0); - useEffect(() => { - const el = document.getElementById("connect-wrap"); - if (!el) return; + // Backend Integration + const [userId, setUserId] = useState(null); + const [messages, setMessages] = useState([]); + const [chats, setChats] = useState([]); + const supabase = createClient(); - const observer = new MutationObserver(() => { - const hasAddress = el.textContent && el.textContent.includes("..."); - setWalletConnected(Boolean(hasAddress)); - }); + useEffect(() => { + async function getUser() { + const { data } = await supabase.auth.getUser(); + if (data?.user) setUserId(data.user.id); + } + getUser(); + }, [supabase.auth]); - observer.observe(el, { - childList: true, - subtree: true, - characterData: true, - }); - return () => observer.disconnect(); + // Fetch rooms + useEffect(() => { + async function loadRooms() { + try { + const res = await fetch("/api/rooms"); + if (res.ok) { + const data = await res.json(); + const formattedRooms = data.rooms.map((r: any) => ({ + id: r.id, + name: r.name, + address: r.created_by, + lastMessage: r.description || "Start chatting", + lastSeen: new Date(r.created_at).toLocaleDateString(), + unreadCount: 0, + status: "online" as PresenceStatus, + })); + setChats(formattedRooms); + } + } catch (err) { + console.error("Failed to load rooms:", err); + } + } + loadRooms(); }, []); - const [selectedChatId, setSelectedChatId] = useState(null) - const [query, setQuery] = useState("") - const [inputMessage, setInputMessage] = useState("") - const [roomMembersOpen, setRoomMembersOpen] = useState(false) - - const [walletConnected, setWalletConnected] = useState(false) - const [currentPublicKey, setCurrentPublicKey] = useState(null) - const [reputationScore, setReputationScore] = useState(0) - - const [messagesByChat, setMessagesByChat] = useState>({ - "1": [ - { - id: "m1", - author: "them", - text: "Hey, welcome to AnonChat 👋", - time: "14:20", - delivered: true, - read: true, - }, - { - id: "m2", - author: "me", - text: "Love how clean this feels on desktop.", - time: "14:22", - delivered: false, - read: false, - status: "sending", - }, - { - id: "m2b", - author: "me", - text: "Just sent another update.", - time: "14:23", - delivered: false, - read: false, - status: "sent", - }, - { - id: "m2c", - author: "me", - text: "Let me know once it lands.", - time: "14:24", - delivered: true, - read: false, - status: "delivered", - }, - { - id: "m2d", - author: "me", - text: "Seen it?", - time: "14:24", - delivered: true, - read: true, - status: "read", - }, - { - id: "m3", - author: "them", - text: "Messages stay end‑to‑end encrypted here.", - time: "14:25", - delivered: true, - read: false, - }, - ], - "2": [ - { - id: "m4", - author: "them", - text: "New governance draft is live.", - time: "09:02", - delivered: true, - read: true, - }, - ], - "3": [ - { - id: "m5", - author: "me", - text: "Let’s catch up on the drop.", - time: "17:40", - delivered: true, - read: true, - }, - ], - }) + + // Fetch messages + subscribe + useEffect(() => { + if (!selectedChatId) return; + + setMessages([]); + + async function loadMessages() { + try { + const res = await fetch(`/api/messages?room_id=${selectedChatId}`); + if (res.ok) { + const data = await res.json(); + const formatted = (data.messages || []) + .map((m: any) => ({ + id: m.id, + author: m.user_id === userId ? "me" : "them", + text: m.content, + time: new Date(m.created_at).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + delivered: true, + read: true, + status: "read", + })) + .reverse(); + setMessages(formatted); + } + } catch (error) { + console.error("Failed to fetch messages:", error); + } + } + loadMessages(); + + const channel = supabase + .channel(`room:${selectedChatId}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "messages", + filter: `room_id=eq.${selectedChatId}`, + }, + (payload) => { + const m = payload.new; + const newMessage: ChatMessage = { + id: m.id, + author: m.user_id === userId ? "me" : "them", + text: m.content, + time: new Date(m.created_at).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + delivered: true, + read: true, + status: "read", + }; + setMessages((prev) => [...prev, newMessage]); + } + ) + .subscribe(); + + return () => { + supabase.removeChannel(channel); + }; + }, [selectedChatId, userId, supabase]); // Update reputation score useEffect(() => { const updateScore = () => { - setReputationScore(calculateReputation(currentPublicKey)) - } - updateScore() - window.addEventListener("reputationUpdate", updateScore) - return () => window.removeEventListener("reputationUpdate", updateScore) - }, [currentPublicKey]) + setReputationScore(calculateReputation(currentPublicKey)); + }; + updateScore(); + window.addEventListener("reputationUpdate", updateScore); + return () => window.removeEventListener("reputationUpdate", updateScore); + }, [currentPublicKey]); // Sync wallet state properly useEffect(() => { const checkWallet = async () => { - const address = await getPublicKey() - setWalletConnected(!!address) - checkWallet() - - // Listen for disconnects - const unsubscribe = onDisconnect(() => { - setWalletConnected(false) - setCurrentPublicKey(null) - }) - - // Heuristic: Check on interval or simple event as well since kit doesn't have onConnect yet - const interval = setInterval(checkWallet, 1000) - - const initialChats: ChatPreview[] = useMemo( - () => [ - { - id: "1", - name: "Anon Whisper", - address: "GABC...1234", - lastMessage: "Got your message, will reply soon.", - lastSeen: "Today • 14:32", - unreadCount: 2, - status: "online", - }, - { - id: "2", - name: "Room #xf23", - address: "GCDE...5678", - lastMessage: "Pinned the latest proposal for review.", - lastSeen: "Today • 09:10", - unreadCount: 0, - status: "recently_active", - }, - { - id: "3", - name: "Collector", - address: "GHJK...9012", - lastMessage: "Let’s sync tomorrow.", - lastSeen: "Yesterday • 18:04", - unreadCount: 0, - status: "offline", - }, - ], - [], - ); - return () => { - unsubscribe() - clearInterval(interval) - } - }}, []) - - const [chats, setChats] = useState([ - { - id: "1", - name: "Anon Whisper", - address: "GABC ... 1234", - lastMessage: "Got your message, will reply soon.", - lastSeen: "Today • 14:32", - unreadCount: 2, - status: "online", - }, - { - id: "2", - name: "Room #xf23", - address: "GCDE ... 5678", - lastMessage: "Pinned the latest proposal for review.", - lastSeen: "Today • 09:10", - unreadCount: 0, - status: "recently_active", - }, - { - id: "3", - name: "Collector", - address: "GHJK ... 9012", - lastMessage: "Let’s sync tomorrow.", - lastSeen: "Yesterday • 18:04", - unreadCount: 0, - status: "offline", - }, - ]); + const address = await getPublicKey(); + setWalletConnected(!!address); + setCurrentPublicKey(address); + }; + checkWallet(); + + // Listen for disconnects + const unsubscribe = onDisconnect(() => { + setWalletConnected(false); + setCurrentPublicKey(null); + }); + + const interval = setInterval(checkWallet, 1000); + + return () => { + unsubscribe(); + clearInterval(interval); + }; + }, []); const markRoomRead = async (roomId: string) => { try { @@ -268,143 +210,88 @@ export default function ChatPage() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ roomId }), - }) + }); } catch (err) { - console.error("Failed to mark room read", err) + console.error("Failed to mark room read", err); } - } + }; const handleSelectChat = async (id: string) => { - setSelectedChatId(id) - // update server and local unread count - await markRoomRead(id) - setChats((prev) => prev.map((c) => (c.id === id ? { ...c, unreadCount: 0 } : c))) - } - - const messagesByChat: Record = useMemo( - () => ({ - "1": [ - { - id: "m1", - author: "them", - text: "Hey, welcome to AnonChat 👋", - time: "14:20", - delivered: true, - read: true, - }, - { - id: "m2", - author: "me", - text: "Love how clean this feels on desktop.", - time: "14:22", - delivered: false, - read: false, - status: "sending", - }, - { - id: "m2b", - author: "me", - text: "Just sent another update.", - time: "14:23", - delivered: false, - read: false, - status: "sent", - }, - { - id: "m2c", - author: "me", - text: "Let me know once it lands.", - time: "14:24", - delivered: true, - read: false, - status: "delivered", - }, - { - id: "m2d", - author: "me", - text: "Seen it?", - time: "14:24", - delivered: true, - read: true, - status: "read", - }, - { - id: "m3", - author: "them", - text: "Messages stay end‑to‑end encrypted here.", - time: "14:25", - delivered: true, - read: false, - }, - ], - "2": [ - { - id: "m4", - author: "them", - text: "New governance draft is live.", - time: "09:02", - delivered: true, - read: true, - }, - ], - "3": [ - { - id: "m5", - author: "me", - text: "Let’s catch up on the drop.", - time: "17:40", - delivered: true, - read: true, - }, - ], - }), - [], - ); + setSelectedChatId(id); + await markRoomRead(id); + setChats((prev) => + prev.map((c) => (c.id === id ? { ...c, unreadCount: 0 } : c)) + ); + }; + // Listen for new room creation useEffect(() => { const handleRoomCreated = (e: any) => { - const newRoom = e.detail - setChats(prev => [newRoom, ...prev]) - setSelectedChatId(newRoom.id) - } - window.addEventListener("roomCreated", handleRoomCreated) - return () => window.removeEventListener("roomCreated", handleRoomCreated) - }, []) + const newRoom = e.detail; + setChats((prev) => [newRoom, ...prev]); + setSelectedChatId(newRoom.id); + }; + window.addEventListener("roomCreated", handleRoomCreated); + return () => window.removeEventListener("roomCreated", handleRoomCreated); + }, []); + + const handleSendMessage = async () => { + if (!inputMessage.trim() || !selectedChatId) return; - const handleSendMessage = () => { - if (!inputMessage.trim() || !selectedChatId) return + const content = inputMessage; + setInputMessage(""); - const newMessage: ChatMessage = { - id: `m${Date.now()}`, + // Optimistically add to messages + const fakeId = "temp-" + Date.now(); + const tempMsg: ChatMessage = { + id: fakeId, author: "me", - text: inputMessage, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }), + text: content, + time: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), delivered: false, read: false, - status: "sent" - } + status: "sending", + }; + setMessages((prev) => [...prev, tempMsg]); - setMessagesByChat(prev => ({ - ...prev, - [selectedChatId]: [...(prev[selectedChatId] || []), newMessage] - })) + try { + await fetch("/api/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ room_id: selectedChatId, content }), + }); + + setChats((prev) => + prev.map((chat) => + chat.id === selectedChatId + ? { ...chat, lastMessage: content, lastSeen: "Just now", unreadCount: 0 } + : chat + ) + ); - setChats(prev => prev.map(chat => - chat.id === selectedChatId - ? { ...chat, lastMessage: inputMessage, lastSeen: "Just now", unreadCount: 0 } - : chat - )) + // We rely on Supabase subscription to stream back the actual message + setMessages((prev) => prev.filter((m) => m.id !== fakeId)); - setInputMessage("") - trackActivity(currentPublicKey, 'message') - } + trackActivity(currentPublicKey, "message"); + } catch (err) { + console.error("Failed to send message:", err); + // Mark as failed visually + setMessages((prev) => + prev.map((m) => (m.id === fakeId ? { ...m, status: "sent" } : m)) + ); + } + }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleSendMessage() + e.preventDefault(); + void handleSendMessage(); } - } + }; const getDeliveryStatus = (message: ChatMessage) => { if (message.status) return message.status; @@ -418,14 +305,13 @@ export default function ChatPage() { const q = query.toLowerCase(); return chats.filter( (c) => - c.name.toLowerCase().includes(q) || c.address.toLowerCase().includes(q), + c.name.toLowerCase().includes(q) || c.address.toLowerCase().includes(q) ); }, [chats, query]); const selectedChat = selectedChatId - ? (chats.find((c) => c.id === selectedChatId) ?? null) + ? chats.find((c) => c.id === selectedChatId) ?? null : null; - const messages = selectedChat ? (messagesByChat[selectedChat.id] ?? []) : []; return (
@@ -433,12 +319,10 @@ export default function ChatPage() {
- {/* Sidebar */} + + {/* WhatsApp-Style Dark Sidebar */}
+
); - - ) }