diff --git a/packages/app/src/follows.ts b/packages/app/src/follows.ts index 837b25d..494dd1d 100644 --- a/packages/app/src/follows.ts +++ b/packages/app/src/follows.ts @@ -1,4 +1,4 @@ -import {FOLLOWS, asDecryptedEvent, readList} from '@welshman/util' +import {FOLLOWS, getListValues, asDecryptedEvent, readList} from '@welshman/util' import {type TrustedEvent, type PublishedList} from '@welshman/util' import {type SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEventsMapped, withGetter} from '@welshman/store' @@ -33,3 +33,6 @@ export const { await load({...request, filters: [{kinds: [FOLLOWS], authors: [pubkey]}]}) }, }) + +export const getFollows = (pubkey: string) => + getListValues("p", followsByPubkey.get().get(pubkey)) diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index d916a5b..e70fda8 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -15,4 +15,5 @@ export * from './storage' export * from './thunk' export * from './topics' export * from './util' +export * from './wot' export * from './zappers' diff --git a/packages/app/src/mutes.ts b/packages/app/src/mutes.ts index 575a420..0ee77af 100644 --- a/packages/app/src/mutes.ts +++ b/packages/app/src/mutes.ts @@ -1,4 +1,4 @@ -import {MUTES, asDecryptedEvent, readList} from '@welshman/util' +import {MUTES, getListValues, asDecryptedEvent, readList} from '@welshman/util' import {type TrustedEvent, type PublishedList} from '@welshman/util' import {type SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEventsMapped, withGetter} from '@welshman/store' @@ -33,3 +33,8 @@ export const { await load({...request, filters: [{kinds: [MUTES], authors: [pubkey]}]}) }, }) + + +export const getMutes = (pubkey: string) => + getListValues("p", mutesByPubkey.get().get(pubkey)) + diff --git a/packages/app/src/profiles.ts b/packages/app/src/profiles.ts index 598219d..e6a8615 100644 --- a/packages/app/src/profiles.ts +++ b/packages/app/src/profiles.ts @@ -1,4 +1,6 @@ +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" @@ -7,6 +9,7 @@ import {repository, load} from './core' import {createSearch} from './util' import {collection} from './collection' import {loadRelaySelections} from './relaySelections' +import {getUserWotScore} from './wot' export const profiles = withGetter( deriveEventsMapped(repository, { @@ -45,11 +48,22 @@ 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}) => + score! < 0.1 ? dec(score!) * getUserWotScore(item.event.pubkey) : -score!, fuseOptions: { keys: ["name", "display_name", {name: "about", weight: 0.3}], + threshold: 0.3, + shouldSort: false, }, }), ) diff --git a/packages/app/src/util.ts b/packages/app/src/util.ts index 6e6777c..bd0cac0 100644 --- a/packages/app/src/util.ts +++ b/packages/app/src/util.ts @@ -5,6 +5,7 @@ import {sortBy} from "@welshman/lib" export type SearchOptions = { getValue: (item: T) => V fuseOptions?: IFuseOptions + onSearch?: (term: string) => void sortFn?: (items: FuseResult) => any } @@ -21,6 +22,8 @@ export const createSearch = (options: T[], opts: SearchOptions): Sea 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, score: 1}) as FuseResult) if (opts.sortFn) { diff --git a/packages/app/src/wot.ts b/packages/app/src/wot.ts new file mode 100644 index 0000000..ec1ea7a --- /dev/null +++ b/packages/app/src/wot.ts @@ -0,0 +1,87 @@ +import {throttle} from 'throttle-debounce' +import {derived, writable} from 'svelte/store' +import {addToMapKey, inc, dec} from '@welshman/lib' +import {getListValues} from '@welshman/util' +import {throttled, withGetter} from '@welshman/store' +import {pubkey} from './session' +import {follows, getFollows, followsByPubkey} from './follows' +import {mutes, getMutes} from './mutes' + +export const followersByPubkey = withGetter( + derived( + throttled(1000, follows), + lists => { + const $followersByPubkey = new Map>() + + for (const list of lists) { + for (const pubkey of getListValues("p", list)) { + addToMapKey($followersByPubkey, pubkey, list.event.pubkey) + } + } + + return $followersByPubkey + } + ) +) + +export const mutersByPubkey = withGetter( + derived( + throttled(1000, mutes), + lists => { + const $mutersByPubkey = new Map>() + + for (const list of lists) { + for (const pubkey of getListValues("p", list)) { + addToMapKey($mutersByPubkey, pubkey, list.event.pubkey) + } + } + + return $mutersByPubkey + } + ) +) + +export const getFollowers = (pubkey: string) => + Array.from(followersByPubkey.get().get(pubkey) || []) + +export const getMuters = (pubkey: string) => + Array.from(mutersByPubkey.get().get(pubkey) || []) + +export const getFollowsWhoFollow = (pubkey: string, target: string) => + getFollows(pubkey).filter(other => getFollows(other).includes(target)) + +export const getFollowsWhoMute = (pubkey: string, target: string) => + getFollows(pubkey).filter(other => getMutes(other).includes(target)) + +export const wotGraph = withGetter(writable(new Map())) + +const buildGraph = throttle(1000, () => { + const $pubkey = pubkey.get() + const $graph = new Map() + const $follows = $pubkey ? getFollows($pubkey) : followsByPubkey.get().keys() + + for (const follow of $follows) { + for (const pubkey of getFollows(follow)) { + $graph.set(pubkey, inc($graph.get(pubkey))) + } + + for (const pubkey of getMutes(follow)) { + $graph.set(pubkey, dec($graph.get(pubkey))) + } + } + + wotGraph.set($graph) +}) + +pubkey.subscribe(buildGraph) +follows.subscribe(buildGraph) +mutes.subscribe(buildGraph) + +export const getWotScore = (pubkey: string, target: string) => { + const follows = pubkey ? getFollowsWhoFollow(pubkey, target) : getFollowers(target) + const mutes = pubkey ? getFollowsWhoMute(pubkey, target) : getMuters(target) + + return follows.length - mutes.length +} + +export const getUserWotScore = (pubkey: string) => wotGraph.get().get(pubkey) || 0 diff --git a/packages/lib/src/LRUCache.ts b/packages/lib/src/LRUCache.ts index 73931ea..84db954 100644 --- a/packages/lib/src/LRUCache.ts +++ b/packages/lib/src/LRUCache.ts @@ -65,5 +65,5 @@ export function cached({ } export function simpleCache(getValue: (args: Args) => V) { - return cached({maxSize: 10**10, getKey: xs => xs.join(':'), getValue}) + return cached({maxSize: 10**5, getKey: xs => xs.join(':'), getValue}) }