Files
beavergram/frontend/src/lib/components/search/ChatSearchPanel.svelte
T

154 lines
3.4 KiB
Svelte

<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { searchMessages } from "$lib/api/endpoints";
import type { SearchHit } from "$lib/api/types";
import SearchMessageItem from "$lib/components/search/SearchMessageItem.svelte";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
import Spinner from "$lib/components/ui/Spinner.svelte";
import { ui } from "$lib/stores/ui.svelte";
const DEBOUNCE_MS = 250;
const chatId = $derived(
page.params.chatId ? Number(page.params.chatId) : null
);
let query = $state("");
let hits = $state<SearchHit[]>([]);
let loading = $state(false);
let timer: ReturnType<typeof setTimeout> | null = null;
let seq = 0;
async function run(value: string, id: number) {
const current = ++seq;
loading = true;
try {
const result = await searchMessages(value, { chat_id: id });
if (current === seq) {
hits = result;
}
} catch {
if (current === seq) {
hits = [];
}
} finally {
if (current === seq) {
loading = false;
}
}
}
function onInput(event: Event) {
query = (event.currentTarget as HTMLInputElement).value;
const value = query.trim();
if (timer) {
clearTimeout(timer);
}
if (value.length === 0 || chatId === null) {
seq++;
hits = [];
loading = false;
return;
}
const id = chatId;
loading = true;
timer = setTimeout(() => {
run(value, id).catch(() => undefined);
}, DEBOUNCE_MS);
}
function openHit(messageId: number) {
if (chatId !== null) {
ui.requestJump(chatId, messageId);
goto(`/app/${chatId}`);
}
}
</script>
<div class="chat-search">
<div class="search-input">
<Icon name="search" size="1.25rem" class="chat-search-icon" />
<input
type="text"
placeholder="Поиск в чате"
value={query}
oninput={onInput}
>
</div>
<div class="results custom-scroll">
{#if loading && hits.length === 0}
<div class="loading"><Spinner /></div>
{:else if hits.length > 0}
{#each hits as hit (`${hit.chat_id}:${hit.message_id}`)}
<SearchMessageItem {hit} onclick={() => openHit(hit.message_id)} />
{/each}
{:else if query.trim()}
<EmptyState title="Ничего не найдено" />
{/if}
</div>
</div>
<style lang="scss">
.chat-search {
display: flex;
flex-direction: column;
height: 100%;
}
.search-input {
display: flex;
flex-shrink: 0;
align-items: center;
gap: 0.5rem;
height: 2.5rem;
margin: 0.625rem;
padding: 0 0.5rem 0 0.75rem;
border-radius: 1.25rem;
background-color: var(--color-background-secondary);
&:focus-within {
background-color: var(--color-background);
box-shadow: inset 0 0 0 1.5px var(--color-primary);
}
}
:global(.chat-search .chat-search-icon) {
flex-shrink: 0;
color: var(--color-icon-secondary);
}
input {
flex: 1;
min-width: 0;
border: 0;
font-size: 0.9375rem;
color: var(--color-text);
background: transparent;
&::placeholder {
color: var(--color-text-secondary);
}
&:focus {
outline: none;
}
}
.results {
overflow-y: auto;
flex: 1;
padding: 0.25rem 0.4375rem;
}
.loading {
display: flex;
justify-content: center;
padding: 2rem 0;
}
</style>