From 00c25d9803554519f284cec73317a40c815afeba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?=
<35082514+alezmad@users.noreply.github.com>
Date: Sat, 2 May 2026 19:23:21 +0100
Subject: [PATCH] feat(web): client-side search filter in topic chat
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A "search" toggle in the chat header opens a small input that
client-filters loaded messages by plaintext match on body or
sender name. Live tail auto-scroll suspends while a query is
active so matches stay visible when new messages arrive.
Server-side fulltext search lands when ciphertext moves to
per-topic symmetric keys in v0.3.0 — until then there's no
server index to query, and the loaded window (last 100 plus
forward stream) covers most "find that thing from earlier"
needs.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../web/src/modules/mesh/topic-chat-panel.tsx | 72 +++++++++++++++++--
1 file changed, 67 insertions(+), 5 deletions(-)
diff --git a/apps/web/src/modules/mesh/topic-chat-panel.tsx b/apps/web/src/modules/mesh/topic-chat-panel.tsx
index 721f094..9a86b82 100644
--- a/apps/web/src/modules/mesh/topic-chat-panel.tsx
+++ b/apps/web/src/modules/mesh/topic-chat-panel.tsx
@@ -180,6 +180,8 @@ export function TopicChatPanel({
start: number;
selected: number;
} | null>(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchOpen, setSearchOpen] = useState(false);
const scrollRef = useRef(null);
const textareaRef = useRef(null);
const seenIdsRef = useRef>(new Set());
@@ -344,11 +346,14 @@ export function TopicChatPanel({
}, [apiKeySecret, topicName, markRead]);
useEffect(() => {
+ // Don't yank scroll while the user is searching — they're reading
+ // matches, not the live tail.
+ if (searchQuery.trim()) return;
scrollRef.current?.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
- }, [messages.length]);
+ }, [messages.length, searchQuery]);
// Member name lookup for autocomplete. Filtered by case-insensitive
// prefix match on displayName; shorter names rank higher so e.g. "@al"
@@ -457,6 +462,19 @@ export function TopicChatPanel({
const onlineCount = members.filter((m) => m.online).length;
+ // Client-side search over loaded messages. Decodes once per query so
+ // we can filter on plaintext, then highlights matches in render.
+ // Server-side fulltext lands when we move ciphertext to per-topic
+ // keys (v0.3.0) — until then there's no server index to query.
+ const searchTerm = searchQuery.trim().toLowerCase();
+ const filteredMessages = useMemo(() => {
+ if (!searchTerm) return messages;
+ return messages.filter((m) =>
+ decodeIncoming(m.ciphertext).toLowerCase().includes(searchTerm) ||
+ (m.senderName ?? "").toLowerCase().includes(searchTerm),
+ );
+ }, [messages, searchTerm]);
+
return (
{/* Header — mono strip, clay-pulse dot, metadata right */}
@@ -470,9 +488,46 @@ export function TopicChatPanel({
#{topicName}