diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 74b328e..21fa46b 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -10,6 +10,7 @@ export * from './profiles' export * from './relays' export * from './relaySelections' export * from './router' +export * from './search' export * from './session' export * from './storage' export * from './sync' diff --git a/packages/app/src/profiles.ts b/packages/app/src/profiles.ts index a16ee0d..a563daf 100644 --- a/packages/app/src/profiles.ts +++ b/packages/app/src/profiles.ts @@ -1,15 +1,11 @@ -import {debounce} from 'throttle-debounce' import {derived, readable} from 'svelte/store' -import {dec} from '@welshman/lib' import {readProfile, displayProfile, displayPubkey, PROFILE} from '@welshman/util' import type {SubscribeRequestWithHandlers} from "@welshman/net" import type {PublishedProfile, TrustedEvent} from "@welshman/util" import {deriveEventsMapped, withGetter} from '@welshman/store' import {repository, load} from './core' -import {createSearch} from './util' import {collection} from './collection' import {loadRelaySelections} from './relaySelections' -import {wotGraph} from './wot' export const profiles = withGetter( deriveEventsMapped(repository, { @@ -49,31 +45,6 @@ export const { }, }) -export const searchProfiles = debounce(500, (search: string) => { - if (search.length > 2) { - load({filters: [{kinds: [PROFILE], search}]}) - } -}) - -export const profileSearch = derived(profiles, $profiles => - createSearch($profiles, { - onSearch: searchProfiles, - getValue: (profile: PublishedProfile) => profile.event.pubkey, - sortFn: ({score, item}) => { - if (score && score > 0.1) return -score! - - const wotScore = wotGraph.get().get(item.event.pubkey) || 0 - - return score ? dec(score) * wotScore : -wotScore - }, - fuseOptions: { - keys: ["name", "display_name", {name: "about", weight: 0.3}], - threshold: 0.3, - shouldSort: false, - }, - }), -) - export const displayProfileByPubkey = (pubkey: string | undefined) => pubkey ? displayProfile(profilesByPubkey.get().get(pubkey), displayPubkey(pubkey)) diff --git a/packages/app/src/relays.ts b/packages/app/src/relays.ts index e446981..7e25650 100644 --- a/packages/app/src/relays.ts +++ b/packages/app/src/relays.ts @@ -4,7 +4,6 @@ import {ctx, groupBy, indexBy, batch, now, uniq, batcher, postJson} from '@welsh import type {RelayProfile} from "@welshman/util" import {normalizeRelayUrl} from "@welshman/util" import {AuthStatus, asMessage, type Connection, type SocketMessage} from '@welshman/net' -import {createSearch} from './util' import {collection} from './collection' export type RelayStats = { @@ -89,15 +88,6 @@ export const { }), }) -export const relaySearch = derived(relays, $relays => - createSearch($relays, { - getValue: (relay: Relay) => relay.url, - fuseOptions: { - keys: ["url", "name", {name: "description", weight: 0.3}], - }, - }), -) - // Utilities for syncing stats from connections to relays type RelayStatsUpdate = [string, (stats: RelayStats) => void] diff --git a/packages/app/src/search.ts b/packages/app/src/search.ts new file mode 100644 index 0000000..25386eb --- /dev/null +++ b/packages/app/src/search.ts @@ -0,0 +1,113 @@ +import Fuse from "fuse.js" +import type {IFuseOptions, FuseResult} from "fuse.js" +import {debounce} from 'throttle-debounce' +import {derived} from 'svelte/store' +import {dec, sortBy} from '@welshman/lib' +import {PROFILE} from '@welshman/util' +import {throttled} from '@welshman/store' +import type {PublishedProfile} from "@welshman/util" +import {load} from './core' +import {wotGraph} from './wot' +import {profiles} from './profiles' +import {topics} from './topics' +import type {Topic} from './topics' +import {relays} from './relays' +import type {Relay} from './relays' +import {handlesByNip05} from './handles' + +export type SearchOptions = { + getValue: (item: T) => V + fuseOptions?: IFuseOptions + onSearch?: (term: string) => void + sortFn?: (items: FuseResult) => any +} + +export type Search = { + options: T[] + getValue: (item: T) => V + getOption: (value: V) => T | undefined + searchOptions: (term: string) => T[] + searchValues: (term: string) => V[] +} + +export const createSearch = (options: T[], opts: SearchOptions): Search => { + const fuse = new Fuse(options, {...opts.fuseOptions, includeScore: true}) + const map = new Map(options.map(item => [opts.getValue(item), item])) + + const search = (term: string) => { + opts.onSearch?.(term) + + let results = term ? fuse.search(term) : options.map(item => ({item}) as FuseResult) + + if (opts.sortFn) { + results = sortBy(opts.sortFn, results) + } + + return results.map(result => result.item) + } + + return { + options, + getValue: opts.getValue, + getOption: (value: V) => map.get(value), + searchOptions: (term: string) => search(term), + searchValues: (term: string) => search(term).map(opts.getValue), + } +} + +export const searchProfiles = debounce(500, (search: string) => { + if (search.length > 2) { + load({filters: [{kinds: [PROFILE], search}]}) + } +}) + +export const profileSearch = derived( + [throttled(800, profiles), throttled(800, handlesByNip05)], + ([$profiles, $handlesByNip05]) => { + // Remove invalid nip05's from profiles + const options = $profiles + .map(p => { + const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey + + return isNip05Valid ? p : {...p, nip05: ""} + }) + + return createSearch(options, { + onSearch: searchProfiles, + getValue: (profile: PublishedProfile) => profile.event.pubkey, + sortFn: ({score, item}) => { + if (score && score > 0.1) return -score! + + const wotScore = wotGraph.get().get(item.event.pubkey) || 0 + + return score ? dec(score) * wotScore : -wotScore + }, + fuseOptions: { + keys: [ + "nip05", + {name: "name", weight: 0.8}, + {name: "display_name", weight: 0.5}, + {name: "about", weight: 0.3}, + ], + threshold: 0.3, + shouldSort: false, + }, + }) + } +) + +export const topicSearch = derived(topics, $topics => + createSearch($topics, { + getValue: (topic: Topic) => topic.name, + fuseOptions: {keys: ["name"]}, + }), +) + +export const relaySearch = derived(relays, $relays => + createSearch($relays, { + getValue: (relay: Relay) => relay.url, + fuseOptions: { + keys: ["url", "name", {name: "description", weight: 0.3}], + }, + }), +) diff --git a/packages/app/src/topics.ts b/packages/app/src/topics.ts index dee9a41..31ae73a 100644 --- a/packages/app/src/topics.ts +++ b/packages/app/src/topics.ts @@ -1,8 +1,6 @@ import {throttle} from 'throttle-debounce' -import {derived} from 'svelte/store' import {inc} from '@welshman/lib' import {custom} from '@welshman/store' -import {createSearch} from './util' import {repository} from './core' export type Topic = { @@ -32,10 +30,3 @@ export const topics = custom(setter => { return () => repository.off("update", onUpdate) }) - -export const topicSearch = derived(topics, $topics => - createSearch($topics, { - getValue: (topic: Topic) => topic.name, - fuseOptions: {keys: ["name"]}, - }), -) diff --git a/packages/app/src/util.ts b/packages/app/src/util.ts index c280373..8525d70 100644 --- a/packages/app/src/util.ts +++ b/packages/app/src/util.ts @@ -1,46 +1,4 @@ -import Fuse from "fuse.js" -import type {IFuseOptions, FuseResult} from "fuse.js" -import {now, int, sortBy, DAY, HOUR, MINUTE} from "@welshman/lib" - -export type SearchOptions = { - getValue: (item: T) => V - fuseOptions?: IFuseOptions - onSearch?: (term: string) => void - sortFn?: (items: FuseResult) => any -} - -export type Search = { - options: T[] - getValue: (item: T) => V - getOption: (value: V) => T | undefined - searchOptions: (term: string) => T[] - searchValues: (term: string) => V[] -} - -export const createSearch = (options: T[], opts: SearchOptions): Search => { - const fuse = new Fuse(options, {...opts.fuseOptions, includeScore: true}) - const map = new Map(options.map(item => [opts.getValue(item), item])) - - const search = (term: string) => { - opts.onSearch?.(term) - - let results = term ? fuse.search(term) : options.map(item => ({item}) as FuseResult) - - if (opts.sortFn) { - results = sortBy(opts.sortFn, results) - } - - return results.map(result => result.item) - } - - return { - options, - getValue: opts.getValue, - getOption: (value: V) => map.get(value), - searchOptions: (term: string) => search(term), - searchValues: (term: string) => search(term).map(opts.getValue), - } -} +import {now, int, DAY, HOUR, MINUTE} from "@welshman/lib" export const secondsToDate = (ts: number) => new Date(ts * 1000)