100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { useMessageStore } from "../stores/message.ts";
|
|
|
|
interface MessageSearchProps {
|
|
channelId: string;
|
|
channelName: string;
|
|
onClose: () => void;
|
|
onJump?: (messageId: string) => void;
|
|
}
|
|
|
|
export function MessageSearch({ channelId, channelName, onClose, onJump }: MessageSearchProps) {
|
|
const [query, setQuery] = useState("");
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
const results = useMessageStore((s) => s.searchResultsByChannel[channelId] || []);
|
|
const searchMessages = useMessageStore((s) => s.searchMessages);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
inputRef.current?.focus();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const t = setTimeout(() => {
|
|
if (query.trim()) {
|
|
searchMessages(channelId, query.trim());
|
|
setSelectedIndex(0);
|
|
}
|
|
}, 200);
|
|
return () => clearTimeout(t);
|
|
}, [query, channelId, searchMessages]);
|
|
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
onClose();
|
|
return;
|
|
}
|
|
if (results.length === 0) return;
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
setSelectedIndex((i) => (i + 1) % results.length);
|
|
} else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
setSelectedIndex((i) => (i - 1 + results.length) % results.length);
|
|
} else if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const msg = results[selectedIndex];
|
|
if (msg && onJump) {
|
|
onJump(msg.id);
|
|
}
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
}, [results, selectedIndex, onClose, onJump]);
|
|
|
|
return (
|
|
<div className="absolute inset-x-0 top-10 z-20 bg-gb-bg border border-gb-bg-t shadow-lg mx-3">
|
|
<div className="px-3 py-2 border-b border-gb-bg-t flex items-center justify-between">
|
|
<span className="text-xs text-gb-orange font-mono">SEARCH #{channelName.toUpperCase()}</span>
|
|
<button onClick={onClose} className="text-xs text-gb-red hover:text-gb-orange">[x]</button>
|
|
</div>
|
|
<div className="p-2">
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder="search messages..."
|
|
className="terminal-input w-full text-sm"
|
|
/>
|
|
</div>
|
|
<div className="max-h-64 overflow-y-auto font-mono text-xs">
|
|
{results.length === 0 && query.trim() && (
|
|
<div className="px-3 py-2 text-gb-fg-f">[no results]</div>
|
|
)}
|
|
{results.map((msg, idx) => (
|
|
<button
|
|
key={msg.id}
|
|
onClick={() => {
|
|
onJump?.(msg.id);
|
|
onClose();
|
|
}}
|
|
className={`w-full text-left px-3 py-2 border-b border-gb-bg-t last:border-b-0 ${
|
|
idx === selectedIndex ? "bg-gb-bg-s" : "hover:bg-gb-bg-s/50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 text-gb-fg-f">
|
|
<span className="text-gb-aqua"><{msg.author_username}></span>
|
|
<span className="text-gb-fg-s">{new Date(msg.created_at).toLocaleString()}</span>
|
|
</div>
|
|
<div className="text-gb-fg truncate">{msg.content}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|