From 77f80678bf6377ffe420dcb1c1a61483a016caf0 Mon Sep 17 00:00:00 2001 From: Nexha-dev Date: Tue, 24 Feb 2026 14:00:21 +0000 Subject: [PATCH] feat: Add rate limiting to prevent spam messages (#72) - Implement rate limiter with 5 messages per 10 seconds per wallet - Add rate limit check in WebSocket client before sending messages - Display user-friendly toast notification when limit exceeded - Implement optimistic UI rollback on rate limit failure - Add automatic cooldown reset with sliding window algorithm - Fix pre-existing syntax errors in chat page Closes #72 --- app/chat/page.tsx | 399 +- lib/rate-limiter.ts | 49 + lib/websocket/chat-hooks.tsx | 9 +- lib/websocket/client.ts | 19 +- lib/websocket/hooks.ts | 2 +- package-lock.json | 7535 +++++++++++++++++++++++++++------- package.json | 5 +- 7 files changed, 6140 insertions(+), 1878 deletions(-) create mode 100644 lib/rate-limiter.ts diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 55227ee..290da81 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -1,15 +1,5 @@ -"use client"; +"use client" -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 { cn } from "@/lib/utils"; import { useEffect, useMemo, useState } from "react" import Image from "next/image" import { Header } from "@/components/header" @@ -31,57 +21,32 @@ import { Phone, Video, MoreVertical, - Paperclip, - Smile, -} from "lucide-react"; Star, } from "lucide-react" import { calculateReputation, trackActivity } from "@/lib/reputation" import { CONFIG } from "@/lib/config" type ChatPreview = { - id: string; - name: string; - address: string; - lastMessage: string; - lastSeen: string; - unreadCount: number; - status: PresenceStatus; -}; + id: string + name: string + address: string + lastMessage: string + lastSeen: string + unreadCount: number + status: PresenceStatus +} type ChatMessage = { - id: string; - author: "me" | "them"; - text: string; - time: string; - delivered: boolean; - read: boolean; - status?: "sending" | "sent" | "delivered" | "read"; -}; + id: string + author: "me" | "them" + text: string + time: string + delivered: boolean + read: boolean + status?: "sending" | "sent" | "delivered" | "read" +} export default function ChatPage() { - const [selectedChatId, setSelectedChatId] = useState(null); - const [query, setQuery] = useState(""); - const [messageText, setMessageText] = useState(""); - - const [walletConnected, setWalletConnected] = useState(false); - - useEffect(() => { - const el = document.getElementById("connect-wrap"); - if (!el) return; - - const observer = new MutationObserver(() => { - const hasAddress = el.textContent && el.textContent.includes("..."); - setWalletConnected(Boolean(hasAddress)); - }); - - observer.observe(el, { - childList: true, - subtree: true, - characterData: true, - }); - return () => observer.disconnect(); - }, []); const [selectedChatId, setSelectedChatId] = useState(null) const [query, setQuery] = useState("") const [inputMessage, setInputMessage] = useState("") @@ -194,38 +159,6 @@ export default function ChatPage() { // 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) @@ -281,85 +214,6 @@ export default function ChatPage() { 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, - }, - ], - }), - [], - ); // Listen for new room creation useEffect(() => { const handleRoomCreated = (e: any) => { @@ -407,25 +261,27 @@ export default function ChatPage() { } const getDeliveryStatus = (message: ChatMessage) => { - if (message.status) return message.status; - if (message.read) return "read"; - if (message.delivered) return "delivered"; - return "sent"; - }; + if (message.status) return message.status + if (message.read) return "read" + if (message.delivered) return "delivered" + return "sent" + } const filteredChats = useMemo(() => { - if (!query.trim()) return chats; - const q = query.toLowerCase(); + if (!query.trim()) return chats + const q = query.toLowerCase() return chats.filter( (c) => - c.name.toLowerCase().includes(q) || c.address.toLowerCase().includes(q), - ); - }, [chats, query]); + c.name.toLowerCase().includes(q) || + c.address.toLowerCase().includes(q), + ) + }, [chats, query]) const selectedChat = selectedChatId - ? (chats.find((c) => c.id === selectedChatId) ?? null) - : null; - const messages = selectedChat ? (messagesByChat[selectedChat.id] ?? []) : []; + ? chats.find((c) => c.id === selectedChatId) ?? null + : null + + const messages = selectedChat ? messagesByChat[selectedChat.id] ?? [] : [] return (
@@ -434,8 +290,6 @@ export default function ChatPage() {
{/* Sidebar */} -