import { useState, useEffect, useRef, useCallback } from "react"; import { Send, Paperclip, Image as ImageIcon, Copy, Check, Users, LogOut, File as FileIcon, X } from "lucide-react"; function makeCode() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let out = ""; for (let i = 0; i < 6; i++) out += chars[Math.floor(Math.random() * chars.length)]; return out; } function nameColor(name) { const colors = ["#e5533d", "#3f8f5f", "#3a6ea5", "#c9862a", "#8a5fc7", "#c2456a", "#2fa39a"]; let hash = 0; for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); return colors[Math.abs(hash) % colors.length]; } function timeLabel(ts) { const d = new Date(ts); return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function resizeImage(file, maxDim = 800, quality = 0.72) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error("Could not read image")); reader.onload = () => { const img = new Image(); img.onerror = () => reject(new Error("Could not decode image")); img.onload = () => { let { width, height } = img; if (width > height && width > maxDim) { height = Math.round((height * maxDim) / width); width = maxDim; } else if (height > maxDim) { width = Math.round((width * maxDim) / height); height = maxDim; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL("image/jpeg", quality)); }; img.src = reader.result; }; reader.readAsDataURL(file); }); } function readFileAsDataURL(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error("Could not read file")); reader.onload = () => resolve(reader.result); reader.readAsDataURL(file); }); } function formatBytes(bytes) { if (bytes < 1024) return bytes + " B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; return (bytes / (1024 * 1024)).toFixed(1) + " MB"; } const MAX_MESSAGES = 60; const MAX_FILE_BYTES = 1.5 * 1024 * 1024; export default function PrivateChatGroup() { const [screen, setScreen] = useState("loading"); // loading, landing, create, join, chat const [mode, setMode] = useState(null); // 'create' | 'join' const [name, setName] = useState(""); const [code, setCode] = useState(""); const [groupName, setGroupName] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [activeGroup, setActiveGroup] = useState(null); // {code, name, members} const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(""); const [copied, setCopied] = useState(false); const [sending, setSending] = useState(false); const [pendingFileError, setPendingFileError] = useState(""); const scrollRef = useRef(null); const fileInputRef = useRef(null); const imageInputRef = useRef(null); const pollRef = useRef(null); // Prefill from last session (personal, not shared) useEffect(() => { (async () => { try { const last = await window.storage.get("lastSession", false); if (last && last.value) { const parsed = JSON.parse(last.value); if (parsed.name) setName(parsed.name); } } catch (e) { // no previous session, that's fine } setScreen("landing"); })(); }, []); const saveLastSession = useCallback(async (n) => { try { await window.storage.set("lastSession", JSON.stringify({ name: n }), false); } catch (e) { // non-critical } }, []); const loadMessages = useCallback(async (groupCode) => { try { const res = await window.storage.get(`messages:${groupCode}`, true); if (res && res.value) { setMessages(JSON.parse(res.value)); } else { setMessages([]); } } catch (e) { setMessages([]); } }, []); // Polling while in chat useEffect(() => { if (screen === "chat" && activeGroup) { loadMessages(activeGroup.code); pollRef.current = setInterval(() => loadMessages(activeGroup.code), 2500); return () => clearInterval(pollRef.current); } }, [screen, activeGroup, loadMessages]); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages, screen]); async function handleCreate() { setError(""); if (!name.trim()) return setError("Enter your name."); if (!groupName.trim()) return setError("Give the group a name."); setBusy(true); try { const newCode = makeCode(); const groupData = { name: groupName.trim(), createdAt: Date.now(), members: [name.trim()] }; await window.storage.set(`group:${newCode}`, JSON.stringify(groupData), true); await window.storage.set(`messages:${newCode}`, JSON.stringify([]), true); await saveLastSession(name.trim()); setActiveGroup({ code: newCode, name: groupData.name, members: groupData.members }); setMessages([]); setScreen("chat"); } catch (e) { setError("Couldn't create the group. Try again."); } finally { setBusy(false); } } async function handleJoin() { setError(""); if (!name.trim()) return setError("Enter your name."); const cleanCode = code.trim().toUpperCase(); if (!cleanCode) return setError("Enter the invite code."); setBusy(true); try { const res = await window.storage.get(`group:${cleanCode}`, true); if (!res || !res.value) { setError("No group found with that code. Double-check it with whoever invited you."); setBusy(false); return; } const groupData = JSON.parse(res.value); if (!groupData.members.includes(name.trim())) { groupData.members = [...groupData.members, name.trim()]; await window.storage.set(`group:${cleanCode}`, JSON.stringify(groupData), true); } await saveLastSession(name.trim()); setActiveGroup({ code: cleanCode, name: groupData.name, members: groupData.members }); await loadMessages(cleanCode); setScreen("chat"); } catch (e) { setError("Couldn't join the group. Try again."); } finally { setBusy(false); } } async function pushMessage(msg) { try { const res = await window.storage.get(`messages:${activeGroup.code}`, true); const current = res && res.value ? JSON.parse(res.value) : []; const next = [...current, msg].slice(-MAX_MESSAGES); await window.storage.set(`messages:${activeGroup.code}`, JSON.stringify(next), true); setMessages(next); } catch (e) { setError("Message failed to send. Check your connection and try again."); } } async function handleSendText() { const text = draft.trim(); if (!text || sending) return; setSending(true); setDraft(""); await pushMessage({ id: Date.now() + "-" + Math.random().toString(36).slice(2, 7), type: "text", sender: name, content: text, ts: Date.now(), }); setSending(false); } async function handleImagePick(e) { const file = e.target.files && e.target.files[0]; e.target.value = ""; if (!file) return; setPendingFileError(""); setSending(true); try { const dataUrl = await resizeImage(file); await pushMessage({ id: Date.now() + "-" + Math.random().toString(36).slice(2, 7), type: "image", sender: name, content: dataUrl, ts: Date.now(), }); } catch (err) { setPendingFileError("Couldn't send that photo. Try a different one."); } finally { setSending(false); } } async function handleFilePick(e) { const file = e.target.files && e.target.files[0]; e.target.value = ""; if (!file) return; setPendingFileError(""); if (file.size > MAX_FILE_BYTES) { setPendingFileError(`That file is ${formatBytes(file.size)}. Please pick something under ${formatBytes(MAX_FILE_BYTES)} for this demo.`); return; } setSending(true); try { const dataUrl = await readFileAsDataURL(file); await pushMessage({ id: Date.now() + "-" + Math.random().toString(36).slice(2, 7), type: "file", sender: name, content: dataUrl, filename: file.name, size: file.size, ts: Date.now(), }); } catch (err) { setPendingFileError("Couldn't send that file. Try again."); } finally { setSending(false); } } function copyInvite() { const inviteText = `Join my chat group "${activeGroup.name}" — use code: ${activeGroup.code}`; if (navigator.clipboard) { navigator.clipboard.writeText(inviteText).catch(() => {}); } setCopied(true); setTimeout(() => setCopied(false), 1800); } function leaveGroup() { clearInterval(pollRef.current); setActiveGroup(null); setMessages([]); setScreen("landing"); setMode(null); setGroupName(""); setCode(""); setError(""); } // ---------- Screens ---------- if (screen === "loading") { return (
Loading…
); } if (screen === "landing" || screen === "create" || screen === "join") { return (

Private Chat Group

Invite-only. Share the code, no signup needed.

{screen === "landing" && (

This is a demo prototype: anyone with the code can join, and data is stored for this artifact only.

)} {screen === "create" && (
{error &&

{error}

}
)} {screen === "join" && (
{error &&

{error}

}
)}
); } // ---------- Chat screen ---------- return (
{/* Header */}
{activeGroup.name}
{activeGroup.members.length} member{activeGroup.members.length !== 1 ? "s" : ""}
{/* Messages */}
{messages.length === 0 && (
No messages yet. Say hi 👋
)} {messages.map((m) => { const mine = m.sender === name; return (
{!mine && ( {m.sender} )}
{m.type === "text" && {m.content}} {m.type === "image" && ( shared )} {m.type === "file" && ( {m.filename} {formatBytes(m.size || 0)} )}
{timeLabel(m.ts)}
); })}
{pendingFileError && (
{pendingFileError}
)} {/* Composer */}
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSendText(); }} placeholder="Type a message…" className="flex-1 min-w-0 border border-stone-300 rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" />
); }