Show a person's most recent pinned note on their profile dialog

This commit is contained in:
Coracle-Bot 2026-09-11 00:37:37 +00:00
parent e8a1f0bba7
commit fc86977a47
5 changed files with 77 additions and 27 deletions

View file

@ -17,6 +17,7 @@
import ProfileAbout from "@app/components/ProfileAbout.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ProfileMenu from "@app/components/ProfileMenu.svelte"
import ProfilePinnedNote from "@app/components/ProfilePinnedNote.svelte"
import ProfileStatus from "@app/components/ProfileStatus.svelte"
import {messagingRelayLists, profiles, relayManagement, user} from "@app/core"
import {deriveUserIsSpaceAdmin} from "@app/management"
@ -138,6 +139,7 @@
<ProfileStatus {pubkey} {url} />
<ProfileAbout {pubkey} {url} />
<ProfileBadges {pubkey} {url} />
<ProfilePinnedNote {pubkey} {url} />
</div>
</ModalBody>
<ModalFooter>

View file

@ -1,18 +1,18 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte"
import {derived, writable} from "svelte/store"
import {writable} from "svelte/store"
import type {Readable} from "svelte/store"
import {sortBy, uniqBy, now} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {NOTE, outbox} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {getReplyTags} from "@welshman/domain"
import {PinLists} from "@welshman/app"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {app, network, router} from "@app/core"
import {router} from "@app/core"
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
import {derivePinnedEvents} from "@app/pins"
type Props = {
pubkey: string
@ -25,30 +25,9 @@
onDestroy(context.cleanup)
const pinnedIds = derived($app.use(PinLists).one(pubkey), $pinList => $pinList?.ids() ?? [])
const pinnedEvents = derivePinnedEvents(pubkey)
// Pinned notes are asked for by id instead of by the feed's filters, so they get a store of
// their own. A feed dedupes and sorts what it holds as it loads it, and writing to its store
// from out here bypasses both.
const pinnedEvents = writable<TrustedEvent[]>([])
$effect(() => {
if ($pinnedIds.length > 0) {
const controller = new AbortController()
relays.then($relays =>
$network.load({
relays: $relays,
filters: [{ids: $pinnedIds}],
signal: controller.signal,
onEvent: event =>
pinnedEvents.update($pinned => uniqBy(e => e.id, $pinned.concat(event))),
}),
)
return () => controller.abort()
}
})
const pinnedIds = $derived($pinnedEvents.map(e => e.id))
let element: HTMLElement | undefined = $state()
let older: Maybe<ReturnType<typeof makeScrollLoader>> = $state()
@ -59,7 +38,7 @@
const feedEvents = $derived(
sortBy(
e => ($pinnedIds.includes(e.id) ? -(now() + e.created_at) : -e.created_at),
e => (pinnedIds.includes(e.id) ? -(now() + e.created_at) : -e.created_at),
uniqBy(e => e.id, [...$pinnedEvents, ...$events]).filter(
e => getReplyTags(e.tags).replies.length === 0,
),

View file

@ -0,0 +1,31 @@
<script lang="ts">
import {NOTE} from "@welshman/util"
import Pin from "@assets/icons/pin.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import {derivePinnedEvents} from "@app/pins"
type Props = {
pubkey: string
url?: string
}
const {pubkey, url}: Props = $props()
const pinnedEvents = derivePinnedEvents(pubkey)
const event = $derived($pinnedEvents.find(e => e.kind === NOTE))
</script>
{#if event}
<div class="flex flex-col gap-2">
<p class="text-content-muted flex items-center gap-2 text-sm">
<Icon icon={Pin} size={4} />
Pinned note
</p>
<NoteCard {event} {url} hideProfile class="card">
<NoteContent {event} {url} minLength={120} maxLength={240} expandMode="inline" />
</NoteCard>
</div>
{/if}

View file

@ -16,6 +16,7 @@ import {
MessagingRelayLists,
MuteLists,
Network,
PinLists,
Profiles,
Reactions,
RelayLists,
@ -143,6 +144,7 @@ export const logger = usePlugin(Logger)
export const messagingRelayLists = usePlugin(MessagingRelayLists)
export const muteLists = usePlugin(MuteLists)
export const network = usePlugin(Network)
export const pinLists = usePlugin(PinLists)
export const profiles = usePlugin(Profiles)
export const reactions = usePlugin(Reactions)
export const relayLists = usePlugin(RelayLists)

36
src/app/pins.ts Normal file
View file

@ -0,0 +1,36 @@
import {derived} from "svelte/store"
import type {Readable} from "svelte/store"
import {getIdFilters, outbox} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {network, pinLists, router} from "@app/core"
import {deriveEvents} from "@app/repository"
export const derivePinnedEvents = (pubkey: string): Readable<TrustedEvent[]> =>
derived(
pinLists.get().one(pubkey),
($pinList, set: (events: TrustedEvent[]) => void) => {
const ids = $pinList?.ids() || []
if (ids.length > 0) {
const controller = new AbortController()
const filters = getIdFilters(ids)
router
.get()
.resolver.relays([outbox(pubkey)])
.then($relays =>
network.get().load({relays: $relays, filters, signal: controller.signal}),
)
const unsubscribe = deriveEvents(filters).subscribe(set)
return () => {
controller.abort()
unsubscribe()
}
}
set([])
},
[] as TrustedEvent[],
)