feat(backend,frontend): search everywhere in buttons, links and message data, a jump from search or an alert lights up the found words and buttons

This commit is contained in:
hh committed 2026-10-23 22:59:29 +00:00
1 parent acfa726e61
commit d660079c02
21 files changed
+840 -76

No files matched your search

@@ -1,6 +1,7 @@
<script lang="ts">
import { untrack } from "svelte";
import { visible } from "#lib/actions/visible.js";
import SearchEverywhere from "#lib/components/search/SearchEverywhere.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";
@@ -15,7 +16,9 @@
const DEBOUNCE_MS = 250;
const pager = createHitPager();
const everywhere = createHitPager();
let query = $state("");
let deep = $state(false);
let typing = $state(false);
let timer: ReturnType<typeof setTimeout> | null = null;
@@ -23,14 +26,30 @@
page.params.chatId ? Number(page.params.chatId) : null
);
const needle = $derived(query.trim());
const empty = $derived(
needle !== "" &&
!(typing || deep || pager.loading || pager.failed) &&
pager.hits.length === 0
);
function run() {
typing = false;
const value = query.trim();
deep = false;
everywhere.run(null);
pager.run(
value && chatId !== null ? { chat_id: chatId, query: value } : null
needle && chatId !== null ? { chat_id: chatId, query: needle } : null
);
}
function searchEverywhere() {
if (chatId === null) {
return;
}
deep = true;
everywhere.run({ chat_id: chatId, everywhere: true, query: needle });
}
$effect(() => {
if (chatId !== null) {
untrack(run);
@@ -46,11 +65,11 @@
timer = setTimeout(run, DEBOUNCE_MS);
}
function openHit(messageId: number) {
function openHit(messageId: number, marks: string[]) {
if (chatId === null) {
return;
}
ui.requestJump(chatId, messageId);
ui.requestJump(chatId, messageId, marks);
if (isMobile()) {
ui.closePanel();
}
@@ -73,9 +92,9 @@
{#each pager.hits as hit (`${hit.chat_id}:${hit.message_id}`)}
<SearchMessageItem
{hit}
href={search.hitHref(hit.chat_id, hit.message_id)}
href={search.hitHref(hit.chat_id, hit.message_id, pager.terms)}
inChat
onclick={() => openHit(hit.message_id)}
onclick={() => openHit(hit.message_id, pager.terms)}
terms={pager.terms}
/>
{/each}
@@ -96,7 +115,19 @@
>{t("search.retry")}</Button
>
</div>
{:else if query.trim() && !typing}
{/if}
<SearchEverywhere
href={(hit) => search.hitHref(hit.chat_id, hit.message_id, [needle])}
inChat
{needle}
onopen={(hit) => openHit(hit.message_id, [needle])}
onstart={searchEverywhere}
pager={everywhere}
ready={!(typing || pager.loading || pager.failed || pager.hasMore)}
shown={pager.hits}
started={deep}
/>
{#if empty}
<EmptyState title={t("search.nothing")} />
{/if}
</div>