Make the chat channel search functional

The conversation header search button was inert. Toggle a search field that
filters the channel's root messages by body text; clearing or pressing Escape
restores the full timeline. Search stays scoped to the open space, so results
respect the membership the user already has.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-28 20:33:26 -04:00
1 parent bc0865ede7
commit 7c5978d5df
1 file changed
+33 -5
@@ -1,4 +1,4 @@
import type { FormEvent, MutableRefObject } from "react";
import { useState, type FormEvent, type MutableRefObject } from "react";
import { AvatarStack, Icon, IconButton } from "../../shared/ui";
import type {
ApprovalRecord,
@@ -53,13 +53,18 @@ export function ChatConversation({
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
composerRef: MutableRefObject<HTMLTextAreaElement | null>;
}) {
const [query, setQuery] = useState("");
const trimmed = query.trim().toLowerCase();
const visibleRoots = trimmed
? rootMessages.filter((message) => message.bodyMd.toLowerCase().includes(trimmed))
: rootMessages;
return (
<section style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--panel)" }}>
{space && <ConversationHeader space={space} participants={participants} />}
{space && <ConversationHeader space={space} participants={participants} query={query} onQueryChange={setQuery} />}
<div style={{ flex: 1, overflow: "auto", padding: "20px 24px 8px" }}>
{space && (
<ChatTimeline
rootMessages={rootMessages}
rootMessages={visibleRoots}
messagesInSpace={messagesInSpace}
threadsInSpace={threadsInSpace}
pendingApprovals={pendingApprovals}
@@ -89,10 +94,21 @@ export function ChatConversation({
function ConversationHeader({
space,
participants,
query,
onQueryChange,
}: {
space: ChatSpaceRecord;
participants: string[];
query: string;
onQueryChange: (next: string) => void;
}) {
const [searchOpen, setSearchOpen] = useState(false);
const toggleSearch = () => {
setSearchOpen((open) => {
if (open) onQueryChange("");
return !open;
});
};
return (
<header className="row" style={{ height: 52, padding: "0 24px", borderBottom: "1px solid var(--border-soft)", flexShrink: 0 }}>
<Icon name={space.visibility === "restricted" ? "lock" : "hash"} size={15} color="var(--text-3)" />
@@ -114,8 +130,20 @@ function ConversationHeader({
{space.topic}
</span>
<div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }}>
{participants.length > 0 && <AvatarStack names={participants.slice(0, 5)} bot />}
<IconButton icon="search" label="Search" />
{searchOpen && (
<input
className="input"
autoFocus
aria-label="Search messages"
placeholder="Search messages"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
onKeyDown={(event) => { if (event.key === "Escape") toggleSearch(); }}
style={{ width: 200, height: 28, padding: "0 10px" }}
/>
)}
{!searchOpen && participants.length > 0 && <AvatarStack names={participants.slice(0, 5)} bot />}
<IconButton icon="search" label="Search" onClick={toggleSearch} />
<IconButton icon="more" label="More" />
</div>
</header>