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

+49 -2
View File
@@ -11,6 +11,7 @@ import type {
SearchQuery,
} from "#lib/api/types.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { MARK_PARAM } from "#lib/stores/feed.svelte.js";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -30,6 +31,7 @@ const DEBOUNCE_MS = 250;
const REMOTE_DELAY_MS = 450;
const CHAT_LIMIT = 30;
const PAGE_SIZE = 30;
export const EVERYWHERE_MIN = 3;
const KINDS = new Set<string>(["private", "group", "channel"]);
const DAY = /^\d{4}-\d{2}-\d{2}$/;
const PARAMS = [
@@ -41,6 +43,7 @@ const PARAMS = [
"from",
"since",
"until",
"everywhere",
] as const;
type Param = (typeof PARAMS)[number];
@@ -90,6 +93,7 @@ export function createHitPager() {
let hits = $state<SearchHit[]>([]);
let terms = $state<string[]>([]);
let cursor = $state<string | null>(null);
let scannedTo = $state<string | null>(null);
let loading = $state(false);
let appending = $state(false);
let failed = $state(false);
@@ -106,6 +110,7 @@ export function createHitPager() {
hits = request.cursor ? [...hits, ...result.hits] : result.hits;
({ terms } = result);
cursor = result.next_cursor;
scannedTo = result.scanned_to;
} catch {
if (current === seq) {
failed = true;
@@ -122,6 +127,7 @@ export function createHitPager() {
seq += 1;
base = request;
cursor = null;
scannedTo = null;
failed = false;
appending = false;
loading = request !== null;
@@ -160,12 +166,21 @@ export function createHitPager() {
run(base);
},
run,
get scannedTo() {
return scannedTo;
},
get terms() {
return terms;
},
};
}
export type HitPager = ReturnType<typeof createHitPager>;
export function hitKey(hit: SearchHit): string {
return `${hit.chat_id}:${hit.message_id}`;
}
function createSearch() {
let open = $state(false);
let draft = $state("");
@@ -177,7 +192,9 @@ function createSearch() {
let lastRequest = "";
let lastText = "";
let seq = 0;
let lastEverywhere = "";
const messages = createHitPager();
const everywhere = createHitPager();
const params = $derived(page.url.searchParams);
const urlQuery = $derived(params.get("q") ?? "");
@@ -192,6 +209,14 @@ function createSearch() {
}
return JSON.stringify([accounts.selectedId, toRequest(query, filters)]);
});
const everywhereRequest = $derived.by(() => {
const query = urlQuery.trim();
if (!params.has("everywhere") || query.length < EVERYWHERE_MIN) {
return "";
}
const deep = { ...toRequest(query, filters), everywhere: true };
return JSON.stringify([accounts.selectedId, deep]);
});
const peerHits = $derived.by(() => {
const shown = new Set(chatHits.map((chat) => chat.chat_id));
return peerResults.filter((item) => !shown.has(item.chat_id));
@@ -272,7 +297,9 @@ function createSearch() {
stopTyping();
lastRequest = "";
lastText = "";
lastEverywhere = "";
messages.run(null);
everywhere.run(null);
runText("");
}
@@ -288,14 +315,21 @@ function createSearch() {
const update = Object.fromEntries(PARAMS.map((key) => [key, null]));
write(update).catch(() => undefined);
},
everywhere,
get everywhereRequest() {
return everywhereRequest;
},
get filtered() {
return filtered;
},
get filters() {
return filters;
},
hitHref(chatId: number, messageId: number): string {
hitHref(chatId: number, messageId: number, marks: string[]): string {
const next = new URLSearchParams({ message: String(messageId) });
for (const mark of marks) {
next.append(MARK_PARAM, mark);
}
for (const key of PARAMS) {
const value = params.get(key);
if (value) {
@@ -315,6 +349,9 @@ function createSearch() {
return request;
},
reset,
searchEverywhere() {
write({ everywhere: "1" }).catch(() => undefined);
},
setFilter(key: "from" | "kind" | "since" | "until", value: string | null) {
write({ [key]: value }).catch(() => undefined);
},
@@ -329,7 +366,11 @@ function createSearch() {
}
timer = setTimeout(() => {
timer = null;
write({ q: draft.trim() ? draft : null })
const same = draft.trim() === urlQuery.trim();
write({
everywhere: same ? params.get("everywhere") : null,
q: draft.trim() ? draft : null,
})
.catch(() => undefined)
.finally(() => {
if (timer === null) {
@@ -352,6 +393,12 @@ function createSearch() {
runText(value);
}
},
syncEverywhere(key: string) {
if (key !== lastEverywhere) {
lastEverywhere = key;
everywhere.run(key ? (JSON.parse(key)[1] as SearchQuery) : null);
}
},
get text() {
return text;
},