feat: use NIP-50 relay-side search with scope selection

This commit is contained in:
Bhavishy
2026-04-02 11:18:41 +05:30
parent 25a69a8191
commit 329ad507f6
+117 -20
View File
@@ -1,15 +1,17 @@
<script lang="ts">
import {tick} from "svelte"
import {createSearch} from "@welshman/app"
import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {MESSAGE} from "@welshman/util"
import {debounce} from "throttle-debounce"
import {request} from "@welshman/net"
import {userSearchRelayList} from "@welshman/app"
import {formatTimestampAsDate, groupBy, now, uniqBy, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util"
import {getRelaysFromList, MESSAGE, sortEventsDesc} from "@welshman/util"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import {fly} from "@lib/transition"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import {deriveEventsForUrl} from "@app/core/state"
import {DEFAULT_RELAYS} from "@app/core/state"
import {goToEvent} from "@app/util/routes"
type Props = {
@@ -19,14 +21,34 @@
const {url, h}: Props = $props()
const spaceMessages = deriveEventsForUrl(
url,
h ? [{kinds: [MESSAGE], "#h": [h]}] : [{kinds: [MESSAGE]}],
)
type SearchScope = "room" | "space" | "everything"
const scopes: SearchScope[] = h ? ["room", "space", "everything"] : ["space", "everything"]
let term = $state("")
let show = $state(false)
let scope = $state<SearchScope>(h ? "room" : "space")
let results = $state<TrustedEvent[]>([])
let loading = $state(false)
let input: HTMLInputElement | undefined = $state()
let currentSearchId = 0
let controller: AbortController | undefined
const searchRelayUrls = $derived(getRelaysFromList($userSearchRelayList))
const effectiveEverythingRelays = $derived(
uniqBy(identity => identity, searchRelayUrls).length > 0
? uniqBy(identity => identity, searchRelayUrls)
: DEFAULT_RELAYS,
)
const relayStatus = $derived(
scope === "room"
? `Using space relay: ${url} (room filter applied).`
: scope === "space"
? `Using space relay: ${url}.`
: searchRelayUrls.length > 0
? `Using your search relays (${effectiveEverythingRelays.length}).`
: `Using default relays (${effectiveEverythingRelays.length}) because no search relays are configured.`,
)
const open = () => {
show = true
@@ -40,20 +62,75 @@
const clear = () => {
term = ""
show = false
loading = false
results = []
controller?.abort()
controller = undefined
}
const getRelayUrls = () => {
if (scope === "everything") {
return effectiveEverythingRelays
}
return [url]
}
const getFilter = (searchTerm: string): Filter => {
if (scope === "room" && h) {
return {kinds: [MESSAGE], "#h": [h], search: searchTerm}
}
return {kinds: [MESSAGE], search: searchTerm}
}
const search = debounce(300, async (searchTerm: string, searchId: number) => {
controller?.abort()
if (!searchTerm.trim()) {
loading = false
results = []
return
}
controller = new AbortController()
loading = true
try {
const events = await request({
relays: getRelayUrls(),
autoClose: true,
signal: controller.signal,
filters: [getFilter(searchTerm.trim())],
})
if (searchId === currentSearchId) {
results = sortEventsDesc(uniqBy((event: TrustedEvent) => event.id, events))
}
} catch (error) {
if (
!(error instanceof DOMException && error.name === "AbortError") &&
searchId === currentSearchId
) {
results = []
}
} finally {
if (searchId === currentSearchId) {
loading = false
}
}
})
const onInput = () => {
show = true
currentSearchId += 1
void search(term, currentSearchId)
}
const searchIndex = $derived.by(() =>
createSearch($spaceMessages, {
getValue: event => event.id,
fuseOptions: {keys: ["content"]},
}),
)
const results = $derived(term ? searchIndex.searchOptions(term) : [])
const setScope = (value: SearchScope) => {
scope = value
currentSearchId += 1
void search(term, currentSearchId)
}
const eventsByAge = $derived(groupBy(e => getAgeSection(e.created_at), results))
@@ -118,14 +195,34 @@
bind:value={term}
class="min-w-0 grow"
type="text"
placeholder={h ? "Search this room..." : "Search this space..."}
placeholder={scope === "room"
? "Search this room..."
: scope === "space"
? "Search this space..."
: "Search everything..."}
oninput={onInput} />
</label>
<div class="flex flex-wrap gap-1">
{#each scopes as value (value)}
<Button
class={value === scope ? "btn btn-neutral btn-xs" : "btn btn-ghost btn-xs"}
onclick={() => setScope(value)}>
{value === "room" ? "Room" : value === "space" ? "Space" : "Everything"}
</Button>
{/each}
</div>
<div class="max-h-[65vh] overflow-y-auto">
<p class="mb-2 text-xs opacity-70">{relayStatus}</p>
{#if !term}
<p class="text-sm opacity-70">
{h ? "Search for messages in this room." : "Search for messages across this space."}
{scope === "room"
? "Search for messages in this room."
: scope === "space"
? "Search for messages in this space."
: "Search across your configured search relays."}
</p>
{:else if loading}
<p class="text-sm opacity-70">Searching...</p>
{:else if eventsByAge.size === 0}
<p class="text-sm opacity-70">No results found.</p>
{:else}