176 lines
4.8 KiB
Svelte
176 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from "svelte";
|
|
import { visible } from "#lib/actions/visible.js";
|
|
import ChatListItem from "#lib/components/ChatListItem.svelte";
|
|
import DiscoverResultItem from "#lib/components/search/DiscoverResultItem.svelte";
|
|
import SearchEverywhere from "#lib/components/search/SearchEverywhere.svelte";
|
|
import SearchFilters from "#lib/components/search/SearchFilters.svelte";
|
|
import SearchMessageItem from "#lib/components/search/SearchMessageItem.svelte";
|
|
import Button from "#lib/components/ui/Button.svelte";
|
|
import EmptyState from "#lib/components/ui/EmptyState.svelte";
|
|
import Spinner from "#lib/components/ui/Spinner.svelte";
|
|
import { t } from "#lib/i18n/locale.svelte.js";
|
|
import { search } from "#lib/stores/search.svelte.js";
|
|
import { ui } from "#lib/stores/ui.svelte.js";
|
|
import { goto } from "$app/navigation";
|
|
import { page } from "$app/state";
|
|
|
|
const { everywhere, messages } = search;
|
|
let scroller = $state<HTMLDivElement | null>(null);
|
|
|
|
$effect(() => {
|
|
const key = search.request;
|
|
untrack(() => {
|
|
search.sync(key);
|
|
scroller?.scrollTo({ top: 0 });
|
|
});
|
|
});
|
|
|
|
$effect(() => {
|
|
const key = search.everywhereRequest;
|
|
untrack(() => search.syncEverywhere(key));
|
|
});
|
|
|
|
const activeChatId = $derived(
|
|
page.params.chatId ? Number(page.params.chatId) : null
|
|
);
|
|
const showChats = $derived(!search.filtered && search.chatHits.length > 0);
|
|
const showPeers = $derived(!search.filtered && search.peerHits.length > 0);
|
|
const hasMessages = $derived(messages.hits.length > 0);
|
|
const needle = $derived(search.text.trim());
|
|
const searchedAll = $derived(
|
|
!(search.typing || messages.loading || messages.failed || messages.hasMore)
|
|
);
|
|
const deep = $derived(search.everywhereRequest !== "");
|
|
const empty = $derived(
|
|
!(
|
|
search.typing ||
|
|
messages.loading ||
|
|
messages.failed ||
|
|
showChats ||
|
|
showPeers ||
|
|
hasMessages ||
|
|
deep
|
|
)
|
|
);
|
|
|
|
function openChat(chatId: number) {
|
|
search.reset();
|
|
goto(`/app/${chatId}`);
|
|
}
|
|
</script>
|
|
|
|
<div class="search-view">
|
|
<SearchFilters />
|
|
<div class="search-results custom-scroll" bind:this={scroller}>
|
|
{#if showChats}
|
|
<div class="section-label">{t("search.chats")}</div>
|
|
{#each search.chatHits as chat (chat.chat_id)}
|
|
<ChatListItem
|
|
{chat}
|
|
onclick={() => openChat(chat.chat_id)}
|
|
selected={chat.chat_id === activeChatId}
|
|
/>
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if showPeers}
|
|
<div class="section-label">{t("search.peers")}</div>
|
|
{#each search.peerHits as item (item.chat_id)}
|
|
<DiscoverResultItem {item} onclick={() => openChat(item.chat_id)} />
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if hasMessages}
|
|
<div class="section-label">{t("search.messages")}</div>
|
|
{#each messages.hits as hit (`${hit.chat_id}:${hit.message_id}`)}
|
|
<SearchMessageItem
|
|
{hit}
|
|
href={search.hitHref(hit.chat_id, hit.message_id, messages.terms)}
|
|
onclick={() =>
|
|
ui.requestJump(hit.chat_id, hit.message_id, messages.terms)}
|
|
terms={messages.terms}
|
|
/>
|
|
{/each}
|
|
{#if messages.hasMore}
|
|
{#key messages.hits.length}
|
|
<div class="sentinel" use:visible={() => messages.more()}></div>
|
|
{/key}
|
|
{/if}
|
|
{#if messages.appending}
|
|
<div class="loading"><Spinner /></div>
|
|
{/if}
|
|
{:else if messages.loading}
|
|
<div class="loading"><Spinner /></div>
|
|
{:else if messages.failed}
|
|
<div class="failed">
|
|
<p>{t("search.failed")}</p>
|
|
<Button onclick={() => messages.retry()} variant="text"
|
|
>{t("search.retry")}</Button
|
|
>
|
|
</div>
|
|
{/if}
|
|
|
|
<SearchEverywhere
|
|
href={(hit) => search.hitHref(hit.chat_id, hit.message_id, [needle])}
|
|
{needle}
|
|
onopen={(hit) => ui.requestJump(hit.chat_id, hit.message_id, [needle])}
|
|
onstart={() => search.searchEverywhere()}
|
|
pager={everywhere}
|
|
ready={searchedAll}
|
|
shown={messages.hits}
|
|
started={deep}
|
|
/>
|
|
|
|
{#if empty}
|
|
<EmptyState title={t("search.nothing")} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.search-view {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.search-results {
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
padding: 0.25rem 0.4375rem;
|
|
}
|
|
|
|
.section-label {
|
|
padding: 0.625rem 0.75rem 0.375rem;
|
|
font-size: 0.875rem;
|
|
font-weight: var(--font-weight-medium);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.sentinel {
|
|
height: 1px;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 1.5rem 0;
|
|
}
|
|
|
|
.failed {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 2rem 1rem;
|
|
|
|
p {
|
|
margin: 0;
|
|
font-size: 0.9375rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
}
|
|
</style>
|