feat(frontend): search results show deleted, edited and earlier version marks with highlighted words, filter chips live in the url, results load on scroll

This commit is contained in:
hh committed 2026-10-19 03:41:44 +00:00
1 parent d45c96c259
commit 9a6bd8a714
15 files changed
+1430 -228

No files matched your search

@@ -1,74 +1,117 @@
<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 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 { messages } = search;
let scroller = $state<HTMLDivElement | null>(null);
$effect(() => {
const key = search.request;
untrack(() => {
search.sync(key);
scroller?.scrollTo({ top: 0 });
});
});
const activeChatId = $derived(
page.params.chatId ? Number(page.params.chatId) : null
);
const hasChats = $derived(search.chatHits.length > 0);
const hasPeers = $derived(search.peerHits.length > 0);
const hasMessages = $derived(search.messageHits.length > 0);
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 empty = $derived(
!(search.loading || hasChats || hasPeers || hasMessages)
!(
search.typing ||
messages.loading ||
messages.failed ||
showChats ||
showPeers ||
hasMessages
)
);
function openChat(chatId: number) {
search.close();
goto(`/app/${chatId}`);
}
function openHit(chatId: number, messageId: number) {
ui.requestJump(chatId, messageId);
search.close();
search.reset();
goto(`/app/${chatId}`);
}
</script>
<div class="search-results custom-scroll">
{#if hasChats}
<div class="section-label">Чаты</div>
{#each search.chatHits as chat (chat.chat_id)}
<ChatListItem
{chat}
onclick={() => openChat(chat.chat_id)}
selected={chat.chat_id === activeChatId}
/>
{/each}
{/if}
<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 hasPeers}
<div class="section-label">Контакты и каналы</div>
{#each search.peerHits as item (item.chat_id)}
<DiscoverResultItem {item} onclick={() => openChat(item.chat_id)} />
{/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 search.loading && !hasMessages}
<div class="loading"><Spinner /></div>
{:else if hasMessages}
<div class="section-label">Сообщения</div>
{#each search.messageHits as hit (`${hit.chat_id}:${hit.message_id}`)}
<SearchMessageItem
{hit}
onclick={() => openHit(hit.chat_id, hit.message_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)}
onclick={() => ui.requestJump(hit.chat_id, hit.message_id)}
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}
{#if empty}
<EmptyState title="Ничего не найдено" />
{/if}
{#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;
@@ -82,9 +125,27 @@
color: var(--color-primary);
}
.sentinel {
height: 1px;
}
.loading {
display: flex;
justify-content: center;
padding: 2rem 0;
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>