This commit is contained in:
@@ -2,29 +2,41 @@ import {writable} from "svelte/store"
|
||||
import type {Readable, Unsubscriber} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {deriveItems, withGetter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
||||
import type {
|
||||
ReadableWithGetter,
|
||||
EventToItem,
|
||||
ItemsByKey,
|
||||
MakeLoadItemOptions,
|
||||
} from "@welshman/store"
|
||||
import {deriveItems, getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
||||
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Stores} from "./stores.js"
|
||||
|
||||
/**
|
||||
* Utility type which allows for using the same value both for hot gets and derived subscriptions
|
||||
*/
|
||||
export type Projection<T> = {
|
||||
get: () => T
|
||||
$: Readable<T>
|
||||
}
|
||||
|
||||
export const projection = <T>($: Readable<T>, get = getter($)) => ({$, get})
|
||||
|
||||
/**
|
||||
* Base class for a reactive, keyed collection of "local" (non-event) data —
|
||||
* things like relay stats or NIP-11 profiles that aren't backed by the
|
||||
* repository. The collection owns its own map.
|
||||
*
|
||||
* `index` (map) and `all` (values) are `Projection`s — subscribe via `.$`,
|
||||
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store
|
||||
* (snapshot with svelte's `get(...)`, or read `get(key)` directly).
|
||||
*/
|
||||
export class ClientData<T> {
|
||||
index = withGetter(writable(new Map<string, T>()))
|
||||
all = withGetter(deriveItems(this.index))
|
||||
protected store = writable(new Map<string, T>())
|
||||
index: Projection<ItemsByKey<T>>
|
||||
all: Projection<T[]>
|
||||
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||
subs: ((key: string, value: Maybe<T>) => void)[] = []
|
||||
|
||||
constructor(protected readonly ctx: IClient) {
|
||||
this.one = makeDeriveItem(this.index)
|
||||
this.index = projection(this.store)
|
||||
this.all = projection(deriveItems(this.store))
|
||||
this.one = makeDeriveItem(this.store)
|
||||
}
|
||||
|
||||
keys = () => this.index.get().keys()
|
||||
@@ -34,7 +46,7 @@ export class ClientData<T> {
|
||||
get = (key: string) => this.index.get().get(key)
|
||||
|
||||
set = (key: string, value: T) => {
|
||||
this.index.update($items => {
|
||||
this.store.update($items => {
|
||||
$items.set(key, value)
|
||||
|
||||
return $items
|
||||
@@ -44,7 +56,7 @@ export class ClientData<T> {
|
||||
}
|
||||
|
||||
delete = (key: string) => {
|
||||
this.index.update($items => {
|
||||
this.store.update($items => {
|
||||
$items.delete(key)
|
||||
|
||||
return $items
|
||||
@@ -56,7 +68,7 @@ export class ClientData<T> {
|
||||
clear = () => {
|
||||
const keys = Array.from(this.index.get().keys())
|
||||
|
||||
this.index.set(new Map())
|
||||
this.store.set(new Map())
|
||||
|
||||
for (const key of keys) {
|
||||
this.emitItem(key, undefined)
|
||||
@@ -102,7 +114,7 @@ export abstract class LoadableData<T> extends ClientData<T> {
|
||||
|
||||
this.load = makeLoadItem(fetch, read, options)
|
||||
this.forceLoad = makeForceLoadItem(fetch, read)
|
||||
this.one = makeDeriveItem(this.index, this.load)
|
||||
this.one = makeDeriveItem(this.store, this.load)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +131,13 @@ export type DerivedDataOptions<T> = {
|
||||
* over `ctx.itemsByKey`, never a duplicated map. Subclasses implement `fetch`
|
||||
* (how to load an item by key from the network) and pass the filters/decoder via
|
||||
* `super`.
|
||||
*
|
||||
* `index` (map) and `all` (values) are `Projection`s — subscribe via `.$`,
|
||||
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store.
|
||||
*/
|
||||
export abstract class DerivedData<T> {
|
||||
index: ReadableWithGetter<ItemsByKey<T>>
|
||||
all: ReadableWithGetter<T[]>
|
||||
index: Projection<ItemsByKey<T>>
|
||||
all: Projection<T[]>
|
||||
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||
@@ -133,21 +148,21 @@ export abstract class DerivedData<T> {
|
||||
protected readonly ctx: IClient,
|
||||
options: DerivedDataOptions<T>,
|
||||
) {
|
||||
this.index = withGetter(
|
||||
ctx.use(Stores).itemsByKey<T>({
|
||||
filters: options.filters,
|
||||
eventToItem: options.eventToItem,
|
||||
getKey: options.getKey,
|
||||
}),
|
||||
)
|
||||
this.all = withGetter(deriveItems(this.index))
|
||||
const index = ctx.use(Stores).itemsByKey<T>({
|
||||
filters: options.filters,
|
||||
eventToItem: options.eventToItem,
|
||||
getKey: options.getKey,
|
||||
})
|
||||
|
||||
this.index = projection(index)
|
||||
this.all = projection(deriveItems(index))
|
||||
|
||||
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
|
||||
const read = (key: string) => this.index.get().get(key)
|
||||
|
||||
this.load = makeLoadItem(fetch, read, options.loadOptions)
|
||||
this.forceLoad = makeForceLoadItem(fetch, read)
|
||||
this.one = makeDeriveItem(this.index, this.load)
|
||||
this.one = makeDeriveItem(index, this.load)
|
||||
}
|
||||
|
||||
keys = () => this.index.get().keys()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {get} from "svelte/store"
|
||||
import {Scope, FeedController} from "@welshman/feeds"
|
||||
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
||||
import type {AdapterContext} from "@welshman/net"
|
||||
@@ -27,11 +26,11 @@ export class Feeds {
|
||||
case Scope.Self:
|
||||
return [$pubkey]
|
||||
case Scope.Follows:
|
||||
return get(this.ctx.use(Wot).follows($pubkey))
|
||||
return this.ctx.use(Wot).follows($pubkey).get()
|
||||
case Scope.Network:
|
||||
return get(this.ctx.use(Wot).network($pubkey))
|
||||
return this.ctx.use(Wot).network($pubkey).get()
|
||||
case Scope.Followers:
|
||||
return get(this.ctx.use(Wot).followers($pubkey))
|
||||
return this.ctx.use(Wot).followers($pubkey).get()
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {tryCatch, batcher, postJson} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {queryProfile, displayNip05} from "@welshman/util"
|
||||
import type {Handle} from "@welshman/util"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {LoadableData} from "./clientData.js"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
|
||||
@@ -61,20 +63,22 @@ export class Handles extends LoadableData<Handle> {
|
||||
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
||||
}
|
||||
|
||||
forPubkey = (pubkey: string, relays: string[] = []) => {
|
||||
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Handle>> => {
|
||||
this.loadForPubkey(pubkey, relays)
|
||||
|
||||
return deriveDeduplicated(
|
||||
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
|
||||
([$handlesByNip05, $profile]) => {
|
||||
if (!$profile?.nip05) return undefined
|
||||
const read = ([$handlesByNip05, $profile]: [ReadonlyMap<string, Handle>, Maybe<{nip05?: string}>]) => {
|
||||
if (!$profile?.nip05) return undefined
|
||||
|
||||
const handle = $handlesByNip05.get($profile.nip05)
|
||||
const handle = $handlesByNip05.get($profile.nip05)
|
||||
|
||||
if (handle?.pubkey !== pubkey) return undefined
|
||||
if (handle?.pubkey !== pubkey) return undefined
|
||||
|
||||
return handle
|
||||
},
|
||||
return handle
|
||||
}
|
||||
|
||||
return projection(
|
||||
deriveDeduplicated([this.index.$, this.ctx.use(Profiles).one(pubkey, relays)], read),
|
||||
() => read([this.index.get(), this.ctx.use(Profiles).get(pubkey)]),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
PROFILE,
|
||||
} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {DerivedData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
@@ -41,13 +42,13 @@ export class Profiles extends DerivedData<ReturnType<typeof readProfile>> {
|
||||
return this.ctx.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
display = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
|
||||
pubkey ? displayProfile(this.get(pubkey), displayPubkey(pubkey)) : ""
|
||||
display = (pubkey: string | undefined, ...args: any[]): Projection<string> => {
|
||||
const read = ($profile: Maybe<ReturnType<typeof readProfile>>) =>
|
||||
pubkey ? displayProfile($profile, displayPubkey(pubkey)) : ""
|
||||
|
||||
deriveDisplay = (pubkey: string | undefined, ...args: any[]): Readable<string> =>
|
||||
pubkey
|
||||
? derived(this.one(pubkey, ...args), $profile =>
|
||||
displayProfile($profile, displayPubkey(pubkey)),
|
||||
)
|
||||
: readable("")
|
||||
return projection(
|
||||
pubkey ? derived(this.one(pubkey, ...args), read) : readable(""),
|
||||
() => read(pubkey ? this.get(pubkey) : undefined),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {fetchone} from "@welshman/lib"
|
||||
import {fetchJson} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {displayRelayUrl, displayRelayProfile} from "@welshman/util"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import {LoadableData} from "./clientData.js"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
|
||||
/**
|
||||
* NIP-11 relay profiles, keyed by url. A "local" loadable collection: items
|
||||
@@ -36,8 +37,9 @@ export class Relays extends LoadableData<RelayProfile> {
|
||||
}
|
||||
}
|
||||
|
||||
display = (url: string) => displayRelayProfile(this.get(url), displayRelayUrl(url))
|
||||
display = (url: string): Projection<string> => {
|
||||
const read = ($relay: Maybe<RelayProfile>) => displayRelayProfile($relay, displayRelayUrl(url))
|
||||
|
||||
deriveDisplay = (url: string) =>
|
||||
derived(this.one(url), $relay => displayRelayProfile($relay, displayRelayUrl(url)))
|
||||
return projection(derived(this.one(url), read), () => read(this.get(url)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export class Searches {
|
||||
|
||||
constructor(readonly ctx: IClient) {
|
||||
this.profileSearch = derived(
|
||||
[throttled(800, this.ctx.use(Profiles).all), throttled(800, this.ctx.use(Handles).index)],
|
||||
[throttled(800, this.ctx.use(Profiles).all.$), throttled(800, this.ctx.use(Handles).index.$)],
|
||||
([$profiles, $handlesByNip05]) => {
|
||||
// Remove invalid nip05's from profiles
|
||||
const options = $profiles.map(p => {
|
||||
@@ -107,7 +107,7 @@ export class Searches {
|
||||
}),
|
||||
)
|
||||
|
||||
this.relaySearch = derived(this.ctx.use(Relays).all, $relays =>
|
||||
this.relaySearch = derived(this.ctx.use(Relays).all.$, $relays =>
|
||||
createSearch($relays, {
|
||||
getValue: (relay: RelayProfile) => relay.url,
|
||||
fuseOptions: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {get} from "svelte/store"
|
||||
import {uniq, remove} from "@welshman/lib"
|
||||
import {
|
||||
getAddress,
|
||||
@@ -32,7 +31,7 @@ export class Tags {
|
||||
"p",
|
||||
pubkey,
|
||||
this.ctx.use(Router).FromPubkey(pubkey).getUrl() || "",
|
||||
get(this.ctx.use(Profiles).display(pubkey)),
|
||||
this.ctx.use(Profiles).display(pubkey).get(),
|
||||
]
|
||||
|
||||
tagEvent = (event: TrustedEvent, url = "", mark = "") => {
|
||||
|
||||
+169
-119
@@ -2,9 +2,9 @@ import {readable, derived} from "svelte/store"
|
||||
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {List} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import type {ReadableWithGetter} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import {projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import {FollowLists} from "./follows.js"
|
||||
import {MuteLists} from "./mutes.js"
|
||||
|
||||
@@ -15,96 +15,104 @@ const listPubkeys = (list: List | undefined) => getPubkeyTagValues(getListTags(l
|
||||
* built from the perspective of the client's user (or, with no user, the union
|
||||
* of every known follow list) and updated reactively as lists change.
|
||||
*
|
||||
* The aggregate `*ByPubkey`/`graph`/`max` fields are long-lived `withGetter`
|
||||
* stores (snapshot with `.get()`). The parameterized methods (`follows`,
|
||||
* `wotScore`, …) return plain on-demand stores — snapshot them with svelte's
|
||||
* `get(...)`.
|
||||
* The aggregate `*ByPubkey`/`graph`/`max` fields and the parameterized methods
|
||||
* (`follows`, `wotScore`, …) are all `Projection`s — subscribe via `.$`, snapshot
|
||||
* via `.get()`.
|
||||
*/
|
||||
export class Wot {
|
||||
followersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
|
||||
mutersByPubkey: ReadableWithGetter<Map<string, Set<string>>>
|
||||
graph: ReadableWithGetter<Map<string, number>>
|
||||
max: ReadableWithGetter<number | undefined>
|
||||
followersByPubkey: Projection<Map<string, Set<string>>>
|
||||
mutersByPubkey: Projection<Map<string, Set<string>>>
|
||||
graph: Projection<Map<string, number>>
|
||||
max: Projection<number | undefined>
|
||||
|
||||
constructor(readonly ctx: IClient) {
|
||||
this.followersByPubkey = withGetter(
|
||||
readable(new Map<string, Set<string>>(), set =>
|
||||
this.ctx.use(FollowLists).index.subscribe(
|
||||
throttle(1000, lists => {
|
||||
const $followersByPubkey = new Map<string, Set<string>>()
|
||||
const followersByPubkeyStore = readable(new Map<string, Set<string>>(), set =>
|
||||
this.ctx.use(FollowLists).index.$.subscribe(
|
||||
throttle(1000, lists => {
|
||||
const $followersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
set($followersByPubkey)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
this.mutersByPubkey = withGetter(
|
||||
readable(new Map<string, Set<string>>(), set =>
|
||||
this.ctx.use(MuteLists).index.subscribe(
|
||||
throttle(1000, lists => {
|
||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
set($mutersByPubkey)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
this.graph = withGetter(
|
||||
readable(new Map<string, number>(), set => {
|
||||
const rebuild = throttle(1000, () => {
|
||||
const $followLists = this.ctx.use(FollowLists).index.get()
|
||||
const $muteLists = this.ctx.use(MuteLists).index.get()
|
||||
const $pubkey = this.ctx.user?.pubkey
|
||||
const $graph = new Map<string, number>()
|
||||
const roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
|
||||
|
||||
for (const follow of roots) {
|
||||
for (const pubkey of listPubkeys($followLists.get(follow))) {
|
||||
$graph.set(pubkey, inc($graph.get(pubkey)))
|
||||
}
|
||||
|
||||
for (const pubkey of listPubkeys($muteLists.get(follow))) {
|
||||
$graph.set(pubkey, dec($graph.get(pubkey)))
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
set($graph)
|
||||
})
|
||||
|
||||
const unsubscribers = [
|
||||
this.ctx.use(FollowLists).index.subscribe(rebuild),
|
||||
this.ctx.use(MuteLists).index.subscribe(rebuild),
|
||||
]
|
||||
|
||||
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
|
||||
}),
|
||||
set($followersByPubkey)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
this.max = withGetter(derived(this.graph, $g => max(Array.from($g.values()))))
|
||||
const mutersByPubkeyStore = readable(new Map<string, Set<string>>(), set =>
|
||||
this.ctx.use(MuteLists).index.$.subscribe(
|
||||
throttle(1000, lists => {
|
||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
set($mutersByPubkey)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const graphStore = readable(new Map<string, number>(), set => {
|
||||
const rebuild = throttle(1000, () => {
|
||||
const $followLists = this.ctx.use(FollowLists).index.get()
|
||||
const $muteLists = this.ctx.use(MuteLists).index.get()
|
||||
const $pubkey = this.ctx.user?.pubkey
|
||||
const $graph = new Map<string, number>()
|
||||
const roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
|
||||
|
||||
for (const follow of roots) {
|
||||
for (const pubkey of listPubkeys($followLists.get(follow))) {
|
||||
$graph.set(pubkey, inc($graph.get(pubkey)))
|
||||
}
|
||||
|
||||
for (const pubkey of listPubkeys($muteLists.get(follow))) {
|
||||
$graph.set(pubkey, dec($graph.get(pubkey)))
|
||||
}
|
||||
}
|
||||
|
||||
set($graph)
|
||||
})
|
||||
|
||||
const unsubscribers = [
|
||||
this.ctx.use(FollowLists).index.$.subscribe(rebuild),
|
||||
this.ctx.use(MuteLists).index.$.subscribe(rebuild),
|
||||
]
|
||||
|
||||
return () => unsubscribers.forEach(unsubscribe => unsubscribe())
|
||||
})
|
||||
|
||||
const maxStore = derived(graphStore, $g => max(Array.from($g.values())))
|
||||
|
||||
this.followersByPubkey = projection(followersByPubkeyStore)
|
||||
this.mutersByPubkey = projection(mutersByPubkeyStore)
|
||||
this.graph = projection(graphStore)
|
||||
this.max = projection(maxStore)
|
||||
}
|
||||
|
||||
follows = (pubkey: string) =>
|
||||
derived(this.ctx.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
follows = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||
|
||||
mutes = (pubkey: string) =>
|
||||
derived(this.ctx.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
network = (pubkey: string) =>
|
||||
derived(this.ctx.use(FollowLists).index, $lists => {
|
||||
mutes = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||
|
||||
return projection(derived(this.ctx.use(MuteLists).index.$, read), () =>
|
||||
read(this.ctx.use(MuteLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
network = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => {
|
||||
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
|
||||
const network = new Set<string>()
|
||||
|
||||
@@ -117,53 +125,95 @@ export class Wot {
|
||||
}
|
||||
|
||||
return Array.from(network)
|
||||
})
|
||||
}
|
||||
|
||||
followers = (pubkey: string) =>
|
||||
derived(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || []))
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
muters = (pubkey: string) =>
|
||||
derived(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || []))
|
||||
followers = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($followers: ReadonlyMap<string, Set<string>>) =>
|
||||
Array.from($followers.get(pubkey) || [])
|
||||
|
||||
followsWhoFollow = (pubkey: string, target: string) =>
|
||||
derived(this.ctx.use(FollowLists).index, $lists =>
|
||||
return projection(derived(this.followersByPubkey.$, read), () =>
|
||||
read(this.followersByPubkey.get()),
|
||||
)
|
||||
}
|
||||
|
||||
muters = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($muters: ReadonlyMap<string, Set<string>>) =>
|
||||
Array.from($muters.get(pubkey) || [])
|
||||
|
||||
return projection(derived(this.mutersByPubkey.$, read), () => read(this.mutersByPubkey.get()))
|
||||
}
|
||||
|
||||
followsWhoFollow = (pubkey: string, target: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) =>
|
||||
listPubkeys($lists.get(pubkey)).filter(other =>
|
||||
listPubkeys($lists.get(other)).includes(target),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
followsWhoMute = (pubkey: string, target: string) =>
|
||||
derived(
|
||||
[this.ctx.use(FollowLists).index, this.ctx.use(MuteLists).index],
|
||||
([$follows, $mutes]) =>
|
||||
listPubkeys($follows.get(pubkey)).filter(other =>
|
||||
listPubkeys($mutes.get(other)).includes(target),
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
followsWhoMute = (pubkey: string, target: string): Projection<string[]> => {
|
||||
const read = ($follows: ReadonlyMap<string, List>, $mutes: ReadonlyMap<string, List>) =>
|
||||
listPubkeys($follows.get(pubkey)).filter(other =>
|
||||
listPubkeys($mutes.get(other)).includes(target),
|
||||
)
|
||||
|
||||
return projection(
|
||||
derived(
|
||||
[this.ctx.use(FollowLists).index.$, this.ctx.use(MuteLists).index.$],
|
||||
([$follows, $mutes]) => read($follows, $mutes),
|
||||
),
|
||||
() => read(this.ctx.use(FollowLists).index.get(), this.ctx.use(MuteLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
wotScore = (pubkey: string, target: string): Projection<number> => {
|
||||
const read = (
|
||||
$follows: ReadonlyMap<string, List>,
|
||||
$mutes: ReadonlyMap<string, List>,
|
||||
$followers: ReadonlyMap<string, Set<string>>,
|
||||
$muters: ReadonlyMap<string, Set<string>>,
|
||||
) => {
|
||||
let follows: string[]
|
||||
let mutes: string[]
|
||||
|
||||
if (pubkey) {
|
||||
const theirFollows = listPubkeys($follows.get(pubkey))
|
||||
|
||||
follows = theirFollows.filter(other => listPubkeys($follows.get(other)).includes(target))
|
||||
mutes = theirFollows.filter(other => listPubkeys($mutes.get(other)).includes(target))
|
||||
} else {
|
||||
follows = Array.from($followers.get(target) || [])
|
||||
mutes = Array.from($muters.get(target) || [])
|
||||
}
|
||||
|
||||
return follows.length - mutes.length
|
||||
}
|
||||
|
||||
return projection(
|
||||
derived(
|
||||
[
|
||||
this.ctx.use(FollowLists).index.$,
|
||||
this.ctx.use(MuteLists).index.$,
|
||||
this.followersByPubkey.$,
|
||||
this.mutersByPubkey.$,
|
||||
],
|
||||
([$follows, $mutes, $followers, $muters]) => read($follows, $mutes, $followers, $muters),
|
||||
),
|
||||
() =>
|
||||
read(
|
||||
this.ctx.use(FollowLists).index.get(),
|
||||
this.ctx.use(MuteLists).index.get(),
|
||||
this.followersByPubkey.get(),
|
||||
this.mutersByPubkey.get(),
|
||||
),
|
||||
)
|
||||
|
||||
wotScore = (pubkey: string, target: string) =>
|
||||
derived(
|
||||
[
|
||||
this.ctx.use(FollowLists).index,
|
||||
this.ctx.use(MuteLists).index,
|
||||
this.followersByPubkey,
|
||||
this.mutersByPubkey,
|
||||
],
|
||||
([$follows, $mutes, $followers, $muters]) => {
|
||||
let follows: string[]
|
||||
let mutes: string[]
|
||||
|
||||
if (pubkey) {
|
||||
const theirFollows = listPubkeys($follows.get(pubkey))
|
||||
|
||||
follows = theirFollows.filter(other => listPubkeys($follows.get(other)).includes(target))
|
||||
mutes = theirFollows.filter(other => listPubkeys($mutes.get(other)).includes(target))
|
||||
} else {
|
||||
follows = Array.from($followers.get(target) || [])
|
||||
mutes = Array.from($muters.get(target) || [])
|
||||
}
|
||||
|
||||
return follows.length - mutes.length
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
batcher,
|
||||
postJson,
|
||||
} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||
import {LoadableData} from "./clientData.js"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
|
||||
@@ -67,13 +69,15 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
||||
}
|
||||
|
||||
forPubkey = (pubkey: string, relays: string[] = []) => {
|
||||
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Zapper>> => {
|
||||
this.loadForPubkey(pubkey, relays)
|
||||
|
||||
return deriveDeduplicated(
|
||||
[this.index, this.ctx.use(Profiles).one(pubkey, relays)],
|
||||
([$zappersByLnurl, $profile]) =>
|
||||
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined,
|
||||
const read = ([$zappersByLnurl, $profile]: [ReadonlyMap<string, Zapper>, Maybe<{lnurl?: string}>]) =>
|
||||
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined
|
||||
|
||||
return projection(
|
||||
deriveDeduplicated([this.index.$, this.ctx.use(Profiles).one(pubkey, relays)], read),
|
||||
() => read([this.index.get(), this.ctx.use(Profiles).get(pubkey)]),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,7 +109,7 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
await Promise.all(zapReceipts.map(zapReceipt => this.validateZapReceipt(zapReceipt, parent))),
|
||||
)
|
||||
|
||||
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
|
||||
validZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Projection<Zap[]> => {
|
||||
const splits = getZapSplits(parent)
|
||||
const profiles = this.ctx.use(Profiles)
|
||||
|
||||
@@ -114,12 +118,7 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
|
||||
}
|
||||
|
||||
const stores: Readable<any>[] = [
|
||||
this.index,
|
||||
...splits.map(split => profiles.one(split.pubkey)),
|
||||
]
|
||||
|
||||
return deriveDeduplicatedByValue(stores, (values: any[]) => {
|
||||
const read = (values: any[]) => {
|
||||
const $zappersByLnurl = values[0] as Map<string, Zapper>
|
||||
const $profiles = values.slice(1) as Array<{lnurl?: string} | undefined>
|
||||
|
||||
@@ -140,6 +139,13 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
return zapper ? zapFromEvent(zapReceipt, zapper) : undefined
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const stores: Readable<any>[] = [this.index.$, ...splits.map(split => profiles.one(split.pubkey))]
|
||||
|
||||
return projection(
|
||||
deriveDeduplicatedByValue(stores, read),
|
||||
() => read([this.index.get(), ...splits.map(split => profiles.get(split.pubkey))]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user