feat: use NIP-50 relay-side search with scope selection
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user