Optimize deriveUserItem, avoid double load of quotes

This commit is contained in:
Jon Staab 2026-07-29 18:24:57 -07:00
parent 2a5849afcb
commit 934f1cdf10
12 changed files with 75 additions and 78 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts">
import {preventDefault} from "@lib/html"
import {RelayLists} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@ -20,7 +21,7 @@
const {next}: Props = $props()
const userRelayList = deriveUserItem($app => $relayLists)
const userRelayList = deriveUserItem(RelayLists)
const back = () => history.back()

View file

@ -2,15 +2,21 @@
import * as nip19 from "nostr-tools/nip19"
import {derived, writable} from "svelte/store"
import {removeUndefined} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {Address, MESSAGE, eventOutbox, relays as relaySelections, seen} from "@welshman/util"
import {
Address,
MESSAGE,
eventOutbox,
getIdFilters,
relays as relaySelections,
seen,
} from "@welshman/util"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {router} from "@app/core"
import {network, router} from "@app/core"
import {deriveEvent} from "@app/repository"
import {entityLink} from "@app/env"
import {goToEvent} from "@app/routes"
@ -26,12 +32,16 @@
const {id, identifier, kind, pubkey, relays = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
const ref = {id, pubkey, kind, identifier, relays}
const hints = removeUndefined([...relays, url])
const mergedRelays = writable(hints)
const quote = deriveEvent(idOrAddress, hints)
// Start with the hints we were handed, then widen to everything the router can work out:
// where the quoted event has been seen, its author's outbox, and — since whoever quoted it
// must have seen it — the relays the quoting event came from.
const mergedRelays = writable(removeUndefined([...relays, url]))
// must have seen it — the relays the quoting event came from. Resolving that may take a
// round trip for the author's relay list, so only load again if the hints came up empty.
$router.resolver
.relays([
...relaySelections(removeUndefined([url])),
@ -39,11 +49,13 @@
eventOutbox(ref),
seen(event, 0.5),
])
.then(urls => mergedRelays.set(urls))
.then(urls => {
mergedRelays.set(urls)
const quote = derived(mergedRelays, ($mergedRelays, set: (event: Maybe<TrustedEvent>) => void) =>
deriveEvent(idOrAddress, $mergedRelays).subscribe(set),
)
if (!$quote) {
$network.load({filters: getIdFilters([idOrAddress]), relays: urls})
}
})
const entity = derived(mergedRelays, $mergedRelays =>
id

View file

@ -1,6 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
import cx from "classnames"
import {Profiles} from "@welshman/app"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl"
@ -17,7 +18,7 @@
import {pushModal} from "@app/modal"
import {notifications} from "@app/notifications"
import {goToChat, makeSpacePath} from "@app/routes"
import {deriveUserItem, profiles} from "@app/core"
import {deriveUserItem} from "@app/core"
type Props = {
children?: Snippet
@ -25,7 +26,7 @@
const {children}: Props = $props()
const userProfile = deriveUserItem($app => $profiles)
const userProfile = deriveUserItem(Profiles)
const chatHandler = () => goToChat()

View file

@ -1,6 +1,6 @@
<script lang="ts">
import {readable} from "svelte/store"
import {clamp} from "@welshman/lib"
import {WotScope} from "@welshman/app"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
@ -14,38 +14,18 @@
const {pubkey: target, isSelf = false}: Props = $props()
const profileDisplay = $profiles.display(target).$
const followers = $wot.followers(target).$
const score = $derived($wot.wotScore($user.pubkey, target).$)
const followsWhoFollow = $derived(
!isSelf && $user.pubkey
? $wot.followsWhoFollow($user.pubkey, target).$
: readable<string[]>([]),
)
const followerCount = $derived($followers.length)
const networkFollowCount = $derived(isSelf ? followerCount : $followsWhoFollow.length)
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
const progress = $derived(isSelf ? undefined : displayScore)
const followers = $wot.followers(target, WotScope.Follows).$
const score = $wot.score(target, WotScope.Follows).$
const display = $derived(isSelf ? $followers.length : clamp([0, 100], $score))
const trustMessage = $derived.by(() => {
if (isSelf) {
if (followerCount === 0) return "No followers in your network yet."
return `Followed by ${followerCount}+ people in your network.`
if ($followers.length > 0) {
return `Followed by ${$followers.length}+ people in your network.`
}
if (networkFollowCount > 0) {
return `Followed by ${networkFollowCount}+ people in your network.`
}
if (displayScore >= 50) return "This user is highly trusted in your network."
if (displayScore >= 10) return "This user has some trust in your network."
return "This user is not well known in your network."
return isSelf
? "No followers in your network yet."
: "This user is not well known in your network."
})
$effect(() => {
@ -66,24 +46,24 @@
<span class="text-sm opacity-75">{isSelf ? "Followers" : "Trust score"}</span>
<span class="text-lg font-semibold">
{#if isSelf}
{displayScore}
{display}
{:else}
{displayScore} / 100
{display} / 100
{/if}
</span>
</div>
{#if !isSelf}
<progress class="progress w-full" value={progress} max="100"></progress>
<progress class="progress w-full" value={display} max="100"></progress>
{/if}
<p class="text-sm opacity-75">{trustMessage}</p>
</div>
{#if $followsWhoFollow.length > 0}
{#if !isSelf && $followers.length > 0}
<div class="flex flex-col gap-2 border-t border-line pt-4">
<p class="text-sm font-medium">People who follow {$profileDisplay}</p>
<ProfileCircles pubkeys={$followsWhoFollow} limit={5} />
<ProfileCircles pubkeys={$followers} limit={5} />
<p class="text-sm opacity-75">
{$followsWhoFollow.length}
{$followsWhoFollow.length === 1 ? "person" : "people"} you follow also follow
{$followers.length}
{$followers.length === 1 ? "person" : "people"} you follow also follow
{$profileDisplay}.
</p>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import {getWalletAddress} from "@welshman/util"
import {Profiles} from "@welshman/app"
import {errorMessage} from "@lib/util"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@ -13,7 +14,7 @@
import {clearModals} from "@app/modal"
import {pushToast} from "@app/toast"
const userProfile = deriveUserItem($app => $profiles)
const userProfile = deriveUserItem(Profiles)
const lud16 = getWalletAddress(wallet.get()!)

View file

@ -4,6 +4,7 @@
import {sleep} from "@welshman/lib"
import {WalletType} from "@welshman/util"
import type {NWCInfo} from "@welshman/util"
import {Profiles} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
@ -23,12 +24,12 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Divider from "@lib/components/Divider.svelte"
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
import {deriveUserItem, profiles} from "@app/core"
import {deriveUserItem} from "@app/core"
import {getWebLn, wallet} from "@app/lightning"
import {pushToast} from "@app/toast"
import {pushModal} from "@app/modal"
const userProfile = deriveUserItem($app => $profiles)
const userProfile = deriveUserItem(Profiles)
const back = () => history.back()

View file

@ -15,6 +15,7 @@
<script lang="ts">
import {clamp} from "@welshman/lib"
import {WotScope} from "@welshman/app"
import {followLists, user, wot} from "@app/core"
interface Props {
@ -27,7 +28,7 @@
const radius = 6
const center = radius + 1
const score = $derived($wot.wotScore($user.pubkey, target).$)
const score = $derived($wot.score(target, WotScope.Follows).$)
const follows = $derived($followLists.one($user.pubkey))
const active = $derived(($follows?.pubkeys() ?? []).includes(target))
const normalizedScore = $derived(clamp([0, max], $score) / max)

View file

@ -3,7 +3,7 @@ import type {Readable} from "svelte/store"
import {always} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {withGetter} from "@welshman/store"
import type {ItemsByKey, ReadableWithGetter} from "@welshman/store"
import type {ReadableWithGetter} from "@welshman/store"
import {
App,
BlockedRelayLists,
@ -37,7 +37,7 @@ import {
appPolicyRelayStats,
appPolicyWraps,
} from "@welshman/app"
import type {AppPolicy, IApp, Projection, Session} from "@welshman/app"
import type {AppPolicy, DerivedPlugin, Plugin, Session} from "@welshman/app"
import type {BaseEventReader, EventWriter, KindFactory} from "@welshman/domain"
import {maybeMakeRelayMockAdapter} from "@lib/test/relayMocks"
import {DEFAULT_RELAYS, DEFAULT_SEARCH_RELAYS, DUFFLEPUD_URL, INDEXER_RELAYS} from "@app/env"
@ -106,17 +106,20 @@ export const user = withGetter(derived(app, $app => User.require($app)))
export const fromApp = <T>(read: ($app: App) => Readable<T>): Readable<T> =>
derived(app, ($app, set: (value: T) => void) => read($app).subscribe(set))
// The signed-in user's entry in a keyed collection, e.g.
// deriveUserItem($app => $app.use(Profiles)).
export const deriveUserItem = <T>(getPlugin: ($app: App) => {index: Projection<ItemsByKey<T>>}) =>
derived(
[app, fromApp($app => getPlugin($app).index.$)],
([$app, $index]: [App, ItemsByKey<T>]) => {
const $pubkey = $app.user?.pubkey
// The signed-in user's entry in a keyed collection, e.g. deriveUserItem(Profiles).
export const deriveUserItem = <T>(Ctor: Plugin<DerivedPlugin<T>>) =>
derived(app, ($app, set: (item: Maybe<T>) => void) => {
let previous: Maybe<T>
return $pubkey ? $index.get($pubkey) : undefined
},
)
return $app.use(Ctor).index.$.subscribe($index => {
const item = $app.user ? $index.get($app.user.pubkey) : undefined
if (item !== previous) {
previous = item
set(item)
}
})
})
export const login = async ($session: Session) => {
const $user = await User.fromSession($session)
@ -135,8 +138,7 @@ export const login = async ($session: Session) => {
// Plugins bound to the current app, so `$profiles` in a component and `profiles.get()` in a
// module both stay pointed at the right one after login swaps it. Flotilla's own plugins expose
// themselves the same way.
export const usePlugin = <T>(Ctor: new (app: IApp) => T) =>
withGetter(derived(app, $app => $app.use(Ctor)))
export const usePlugin = <T>(Ctor: Plugin<T>) => withGetter(derived(app, $app => $app.use(Ctor)))
export const blockedRelayLists = usePlugin(BlockedRelayLists)
export const blossomServerLists = usePlugin(BlossomServerLists)

View file

@ -1,9 +1,8 @@
import {get, writable} from "svelte/store"
import {on, call, dissoc, assoc, noop, uniq} from "@welshman/lib"
import {on, call, dissoc, assoc, uniq} from "@welshman/lib"
import {isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util"
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
import {
AuthStatus,
SocketEvent,
isRelayEvent,
isRelayOk,
@ -15,7 +14,6 @@ import {
isClientNegOpen,
isClientNegClose,
} from "@welshman/net"
import {merged} from "@welshman/store"
import {
BlockedRelayLists,
MessagingRelayLists,

View file

@ -138,7 +138,7 @@ export const publishRoomQuote = async ({
// User
export const userRoomList = deriveUserItem($app => $app.use(RoomLists))
export const userRoomList = deriveUserItem(RoomLists)
export const userSpaceUrls = derived(userRoomList, $userRoomList => $userRoomList?.urls() ?? [])

View file

@ -96,13 +96,13 @@ const pullAndListen = ({url, signal, filters}: SyncOpts) => {
})
}
const userRoomList = deriveUserItem($app => $app.use(RoomLists))
const userRoomList = deriveUserItem(RoomLists)
const userRelayList = deriveUserItem($app => $app.use(RelayLists))
const userRelayList = deriveUserItem(RelayLists)
const userFollowList = deriveUserItem($app => $app.use(FollowLists))
const userFollowList = deriveUserItem(FollowLists)
const userMessagingRelayList = deriveUserItem($app => $app.use(MessagingRelayLists))
const userMessagingRelayList = deriveUserItem(MessagingRelayLists)
const getSpaceUrls = ($roomList: Maybe<RoomListReader>) =>
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? [])

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {publish} from "@welshman/app"
import {BlossomServerLists, MuteLists, publish} from "@welshman/app"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import {preventDefault} from "@lib/html"
@ -17,8 +17,8 @@
import {PLATFORM_NAME} from "@app/env"
import {userSettingsValues, publishSettings} from "@app/settings"
const userMuteList = deriveUserItem($app => $muteLists)
const userBlossomServerList = deriveUserItem($app => $blossomServerLists)
const userMuteList = deriveUserItem(MuteLists)
const userBlossomServerList = deriveUserItem(BlossomServerLists)
const reset = () => {
settings = {...$userSettingsValues}