Add feed context to improve performance of ReactionSummary

This commit is contained in:
Jon Staab 2026-08-29 09:19:01 -07:00
parent 7d436c482c
commit 0dab2cc779
42 changed files with 468 additions and 153 deletions

View file

@ -7,6 +7,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
@ -18,9 +19,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
const article = $derived(reader(Article)(event))
const h = $derived(article.room())
@ -46,7 +48,13 @@
{/each}
</div>
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -7,15 +7,17 @@
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ArticleActions from "@app/components/ArticleActions.svelte"
import type {FeedContext} from "@app/feeds"
import {reader} from "@app/core"
import {makeArticlePath} from "@app/routes"
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const article = $derived(reader(Article)(event))
const title = $derived(article.title())
@ -52,7 +54,7 @@
Written by
<ProfileLink pubkey={event.pubkey} {url} />
</span>
<ArticleActions showRoom showActivity {url} {event} />
<ArticleActions showRoom showActivity {url} {event} {context} />
</div>
</div>
</div>

View file

@ -8,6 +8,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
@ -21,9 +22,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
const h = $derived(tagValue(tagSpec("h"), event.tags))
const path = $derived(makeCalendarPath(url, getAddress(event)))
@ -42,7 +44,13 @@
</Link>
{/if}
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -3,6 +3,7 @@
import {getAddress, tagValue, tagSpec} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
import type {FeedContext} from "@app/feeds"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import RoomLink from "@app/components/RoomLink.svelte"
@ -11,9 +12,10 @@
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const h = $derived(tagValue(tagSpec("h"), event.tags))
</script>
@ -29,6 +31,6 @@
in <RoomLink {url} {h} />
{/if}
</span>
<CalendarEventActions showActivity {url} {event} />
<CalendarEventActions showActivity {url} {event} {context} />
</div>
</Link>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {goto} from "$app/navigation"
import {
ago,
@ -19,7 +19,7 @@
} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
import {makeEvent, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
import {makeEvent, userInbox, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
import {parse, isLink} from "@welshman/content"
import {MessagingRelayLists, Thunks} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
@ -41,9 +41,10 @@
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import {app, deletes, user, wraps} from "@app/core"
import {app, deletes, router, user, wraps} from "@app/core"
import {userSettingsValues} from "@app/settings"
import {deriveChat, makeChatId} from "@app/chats"
import {makeFeedContext} from "@app/feeds"
import {pushModal} from "@app/modal"
import {DraftKey, type Draft} from "@app/drafts"
import {prependParent} from "@app/rooms"
@ -58,6 +59,9 @@
const {pubkeys, info}: Props = $props()
const chatId = makeChatId(pubkeys)
const context = makeFeedContext({relays: $router.resolver.relays([userInbox()])})
onDestroy(context.cleanup)
const chat = deriveChat(chatId)
const draftKey = new DraftKey<Draft>(`dm:${chatId}`)
const others = remove($user.pubkey, pubkeys)
@ -313,6 +317,7 @@
event={$state.snapshot(value as TrustedEvent)}
{pubkeys}
{showPubkey}
{context}
{replyTo}
canEdit={canEditEvent}
onEdit={onEditEvent} />

View file

@ -14,6 +14,7 @@
import {publishWrappedReaction, retractWrappedReaction} from "@app/reactions"
import Content from "@app/components/Content.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
@ -29,9 +30,10 @@
onEdit?: (event: TrustedEvent) => void
pubkeys: string[]
showPubkey?: boolean
context: FeedContext
}
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false, context}: Props = $props()
const isOwn = event.pubkey === $user.pubkey
const profileDisplay = $profiles.display(event.pubkey).$
@ -117,7 +119,7 @@
</div>
</TapTarget>
<div class="flex gap-2 z-feature -mt-4 ml-4">
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
<ReactionSummary {event} {context} {deleteReaction} {createReaction} noTooltip />
</div>
</div>
</div>

View file

@ -11,6 +11,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ClassifiedStatus from "@app/components/ClassifiedStatus.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
@ -25,9 +26,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
// Editing a listing hands this a new version of the event, so every value read off it has to be
// recomputed rather than captured when the component was created.
@ -60,7 +62,13 @@
{#snippet status()}
<ClassifiedStatus {event} />
{/snippet}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -10,15 +10,17 @@
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
import type {FeedContext} from "@app/feeds"
import RoomLink from "@app/components/RoomLink.svelte"
import {makeClassifiedPath} from "@app/routes"
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const classified = $derived(reader(Classified)(event))
const title = $derived(classified.title())
@ -59,6 +61,6 @@
in <RoomLink {url} {h} />
{/if}
</span>
<ClassifiedActions showActivity {url} {event} />
<ClassifiedActions showActivity {url} {event} {context} />
</div>
</Link>

View file

@ -2,6 +2,7 @@
import type {TrustedEvent, EventContent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import {publishReaction, retractReaction} from "@app/reactions"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
@ -13,9 +14,10 @@
event: TrustedEvent
segment?: string
showActivity?: boolean
context: FeedContext
}
const {url, event, segment, showActivity = false}: Props = $props()
const {url, event, segment, showActivity = false, context}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags)
@ -29,7 +31,13 @@
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity && path}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -8,6 +8,7 @@
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import CommentActions from "@app/components/CommentActions.svelte"
import type {FeedContext} from "@app/feeds"
import type {CommentNode} from "@app/social"
type Props = {
@ -16,9 +17,10 @@
replyTo?: TrustedEvent
setReplyTo: (comment?: TrustedEvent) => void
url?: string
context: FeedContext
}
const {node, root, replyTo, setReplyTo, url}: Props = $props()
const {node, root, replyTo, setReplyTo, url, context}: Props = $props()
const composing = $derived(replyTo?.id === node.comment.id)
@ -37,7 +39,7 @@
<Icon icon={Reply} />
Reply
</Button>
<CommentActions event={node.comment} {url} />
<CommentActions event={node.comment} {url} {context} />
</div>
{/if}
</div>
@ -55,7 +57,7 @@
{#if node.children.length > 0}
<div class="flex flex-col gap-3 border-l border-solid border-line ml-4 pl-4">
{#each node.children as child (child.comment.id)}
<CommentTree node={child} {root} {replyTo} {setReplyTo} {url} />
<CommentTree node={child} {root} {replyTo} {setReplyTo} {url} {context} />
{/each}
</div>
{/if}

View file

@ -8,6 +8,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import CommentTree from "@app/components/CommentTree.svelte"
import type {FeedContext} from "@app/feeds"
import CommentCompose from "@app/components/CommentCompose.svelte"
import {network} from "@app/core"
import {deriveEventsById} from "@app/repository"
@ -16,9 +17,10 @@
type Props = {
event: TrustedEvent
url?: string
context: FeedContext
}
const {event, url}: Props = $props()
const {event, url, context}: Props = $props()
const relays = removeUndefined([url])
const filters = getCommentFiltersForRoot([event])
@ -49,7 +51,7 @@
<div class="flex flex-col gap-3">
{#each nodes as node (node.comment.id)}
<CommentTree {node} root={event} {replyTo} {setReplyTo} {url} />
<CommentTree {node} root={event} {replyTo} {setReplyTo} {url} {context} />
{/each}
</div>
{#if url}

View file

@ -4,6 +4,7 @@
import Link from "@lib/components/Link.svelte"
import {reader} from "@app/core"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import {publishReaction, retractReaction} from "@app/reactions"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
@ -16,9 +17,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
const path = makeGoalPath(url, event.id)
const goal = reader(ZapGoal)(event)
@ -37,7 +39,13 @@
</Link>
{/if}
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -6,6 +6,7 @@
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import GoalActions from "@app/components/GoalActions.svelte"
import type {FeedContext} from "@app/feeds"
import GoalSummary from "@app/components/GoalSummary.svelte"
import RoomLink from "@app/components/RoomLink.svelte"
import {makeGoalPath} from "@app/routes"
@ -13,9 +14,10 @@
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const goal = reader(ZapGoal)(event)
@ -42,6 +44,6 @@
in <RoomLink {url} {h} />
{/if}
</span>
<GoalActions showActivity {url} {event} />
<GoalActions showActivity {url} {event} {context} />
</div>
</Link>

View file

@ -10,15 +10,17 @@
import {publishReaction, retractReaction} from "@app/reactions"
import NoteCard from "@app/components/NoteCard.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import {router} from "@app/core"
type Props = {
event: TrustedEvent
children?: Snippet
context: FeedContext
url?: string
}
const {url, event, children}: Props = $props()
const {url, event, children, context}: Props = $props()
const getRelays = () => (url ? [url] : $router.resolver.relays([seen(event)]))
@ -34,7 +36,13 @@
<NoteCard {event} {url} class="cv card card-interactive">
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-right">
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-right">
<EmojiButton {onEmoji} class="button button-neutral button-xs h-[26px] rounded-2xl">
<Icon icon={SmileCircle} size={4} />
</EmojiButton>

View file

@ -1,7 +1,9 @@
<script lang="ts">
import {onDestroy} from "svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {makeFeedContext} from "@app/feeds"
import {deriveEvent} from "@app/repository"
import {goToEvent} from "@app/routes"
@ -14,11 +16,14 @@
const {url, value, relays}: Props = $props()
const event = deriveEvent(value, relays)
const context = makeFeedContext({relays})
onDestroy(context.cleanup)
</script>
{#if $event}
<Button onclick={() => goToEvent($event)}>
<NoteItem {url} event={$event} />
<NoteItem {url} {context} event={$event} />
</Button>
{:else}
<p class="flex justify-center py-8">

View file

@ -5,6 +5,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
@ -15,9 +16,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags)
const path = makePollPath(url, event.id)
@ -34,7 +36,13 @@
</Link>
{/if}
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -4,6 +4,7 @@
import Link from "@lib/components/Link.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import PollActions from "@app/components/PollActions.svelte"
import type {FeedContext} from "@app/feeds"
import RoomLink from "@app/components/RoomLink.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import {makePollPath} from "@app/routes"
@ -11,9 +12,10 @@
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags)
</script>
@ -29,6 +31,6 @@
in <RoomLink {url} {h} />
{/if}
</span>
<PollActions showActivity {url} {event} />
<PollActions showActivity {url} {event} {context} />
</div>
</Link>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {sortBy, uniqBy} from "@welshman/lib"
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE} from "@welshman/util"
@ -11,6 +11,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {app} from "@app/core"
import {makeFeedContext} from "@app/feeds"
type Props = {
url: string
@ -21,6 +22,10 @@
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
const ctrl = $app.use(Feeds).makeFeedController({
useWindowing: true,
feed: makeIntersectionFeed(
@ -30,6 +35,7 @@
onEvent: (event: TrustedEvent) => {
if (getReplyTags(event.tags).replies.length === 0) {
buffer.push(event)
context.add(event)
}
},
})
@ -64,7 +70,7 @@
<div class="flex flex-col gap-2">
{#each events as event (event.id)}
<div in:fly>
<NoteItem {url} {event} />
<NoteItem {url} {event} {context} />
</div>
{/each}
{#if !hideLoading}

View file

@ -1,9 +1,11 @@
<script lang="ts">
import {onDestroy} from "svelte"
import type {Snippet} from "svelte"
import {NOTE} from "@welshman/util"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
interface Props {
url: string
@ -14,6 +16,10 @@
const {url, pubkey, limit = 1, fallback}: Props = $props()
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
const events = $network.load({
relays: [url],
filters: [{authors: [pubkey], kinds: [NOTE], limit}],
@ -28,7 +34,7 @@
</p>
{:then events}
{#each events as event (event.id)}
<NoteItem {url} {event} />
<NoteItem {url} {event} {context} />
{:else}
<div class="min-h-6">
{@render fallback?.()}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {derived, writable} from "svelte/store"
import type {Writable} from "svelte/store"
import {sortBy, uniqBy, now} from "@welshman/lib"
@ -11,7 +11,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {app, network, router} from "@app/core"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
type Props = {
pubkey: string
@ -20,6 +20,10 @@
const {pubkey}: Props = $props()
const relays = $router.resolver.relays([outbox(pubkey)])
const context = makeFeedContext({relays})
onDestroy(context.cleanup)
const pinnedIds = derived($app.use(PinLists).one(pubkey), $pinList => $pinList?.ids() ?? [])
$effect(() => {
@ -58,6 +62,7 @@
relays: $relays,
element: element!,
filters: [{kinds: [NOTE], authors: [pubkey]}],
onEvent: context.add,
onBackwardExhausted: () => {
exhausted = true
},
@ -74,7 +79,7 @@
<div class="flex flex-col gap-4" bind:this={element}>
{#each feedEvents as event (event.id)}
<div in:fly>
<NoteItem {event} />
<NoteItem {event} {context} />
</div>
{:else}
{#if exhausted}

View file

@ -1,12 +1,10 @@
<script lang="ts">
import cx from "classnames"
import {onMount} from "svelte"
import {derived} from "svelte/store"
import type {Snippet} from "svelte"
import {
batch,
call,
displayList,
filter,
groupBy,
map,
removeUndefined,
@ -15,17 +13,7 @@
uniq,
uniqBy,
} from "@welshman/lib"
import {
REPORT,
REACTION,
ZAP_RECEIPT,
getReplyFilters,
fromMsats,
matchTag,
tagSpec,
userInbox,
DELETE,
} from "@welshman/util"
import {REPORT, REACTION, ZAP_RECEIPT, fromMsats, matchTag, tagSpec} from "@welshman/util"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getEmojis} from "@welshman/domain"
import type {Zap} from "@welshman/domain"
@ -34,13 +22,12 @@
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {deriveEvents} from "@app/repository"
import Reaction from "@app/components/Reaction.svelte"
import ReportDetails from "@app/components/ReportDetails.svelte"
import ProfileList from "@app/components/ProfileList.svelte"
import ZapModal from "@app/components/Zap.svelte"
import {REACTION_KINDS} from "@app/content"
import {app, network, router, user} from "@app/core"
import {app, user} from "@app/core"
import type {FeedContext} from "@app/feeds"
import {deriveUserIsSpaceAdmin} from "@app/management"
import {pushModal} from "@app/modal"
import {deriveDisplaysByPubkey} from "@app/social"
@ -53,6 +40,7 @@
reactionClass?: string
noTooltip?: boolean
innerEvent?: TrustedEvent
context: FeedContext
children?: Snippet
}
@ -64,16 +52,19 @@
reactionClass = "",
noTooltip = false,
innerEvent = undefined,
context,
children,
}: Props = $props()
const eventIds = innerEvent ? [event.id, innerEvent.id] : [event.id]
const ownRelated = context.related(event)
const reports = deriveEvents([{kinds: [REPORT], "#e": [event.id]}])
const related = innerEvent
? derived([ownRelated, context.related(innerEvent)], ([$own, $inner]) => [...$own, ...$inner])
: ownRelated
const reactions = deriveEvents([{kinds: [REACTION], "#e": eventIds}])
const receipts = deriveEvents([{kinds: [ZAP_RECEIPT], "#e": eventIds}])
const reports = derived(ownRelated, $ownRelated => filter(spec({kind: REPORT}), $ownRelated))
const reactions = derived(related, $related => filter(spec({kind: REACTION}), $related))
const receipts = derived(related, $related => filter(spec({kind: ZAP_RECEIPT}), $related))
// A receipt can be a zap of either the event or the one it wraps
const zaps = derived<typeof receipts, Zap[]>(
@ -155,32 +146,6 @@
)
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps.values()))
onMount(() => {
const controller = new AbortController()
call(async () => {
const relays = url ? [url] : await $router.resolver.relays([userInbox()])
if (relays.length > 0) {
$network.load({
relays,
signal: controller.signal,
filters: getReplyFilters([event], {kinds: REACTION_KINDS}),
onEvent: batch(300, (events: TrustedEvent[]) => {
$network.load({
relays,
filters: getReplyFilters(events, {kinds: [DELETE]}),
})
}),
})
}
})
return () => {
controller.abort()
}
})
</script>
{#if $reactions.length > 0 || $zaps.length || $reports.length > 0 || children}

View file

@ -8,30 +8,32 @@
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
import PollItem from "@app/components/PollItem.svelte"
import RecentConversation from "@app/components/RecentConversation.svelte"
import type {FeedContext} from "@app/feeds"
import type {RecentActivityItem} from "@app/recent"
type Props = {
url: string
item: RecentActivityItem
context: FeedContext
}
const {url, item}: Props = $props()
const {url, item, context}: Props = $props()
</script>
{#if item.type === "message"}
<RecentConversation {url} event={item.event} count={item.count} />
{:else if item.event.kind === THREAD}
<ThreadItem {url} event={item.event} />
<ThreadItem {url} {context} event={item.event} />
{:else if item.event.kind === CLASSIFIED}
<ClassifiedItem {url} event={item.event} />
<ClassifiedItem {url} {context} event={item.event} />
{:else if item.event.kind === LONG_FORM}
<ArticleItem {url} event={item.event} />
<ArticleItem {url} {context} event={item.event} />
{:else if item.event.kind === ZAP_GOAL}
<GoalItem {url} event={item.event} />
<GoalItem {url} {context} event={item.event} />
{:else if item.event.kind === EVENT_TIME}
<CalendarEventItem {url} event={item.event} />
<CalendarEventItem {url} {context} event={item.event} />
{:else if item.event.kind === POLL}
<PollItem {url} event={item.event} />
<PollItem {url} {context} event={item.event} />
{:else}
<NoteItem {url} event={item.event} />
<NoteItem {url} {context} event={item.event} />
{/if}

View file

@ -41,7 +41,7 @@
prependParent,
} from "@app/rooms"
import {userSettingsValues} from "@app/settings"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
import {checked, deferredRoomPath, setChecked} from "@app/notifications"
import {makeRoomPath} from "@app/routes"
import {pendingShare, type Share} from "@app/share"
@ -54,6 +54,10 @@
const {url, h}: Props = $props()
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
const room = h ? $rooms.forRoom(url, h) : readable(undefined)
const addMemberKind = h ? ROOM_ADD_MEMBER : RELAY_ADD_MEMBER
const isVoiceRoom = $derived($room && getRoomType($room) === RoomType.Voice)
@ -427,6 +431,7 @@
filters: [
h ? {kinds: [MESSAGE, addMemberKind], "#h": [h]} : {kinds: [MESSAGE, addMemberKind]},
],
onEvent: context.add,
onBackwardExhausted: () => {
loadingBackward = false
},
@ -575,6 +580,7 @@
{event}
{replyTo}
{showPubkey}
{context}
canEdit={canEditEvent}
onEdit={onEditEvent} />
{/if}

View file

@ -41,6 +41,7 @@
import {profiles, thunks, user} from "@app/core"
import {colors} from "@app/theme"
import {ENABLE_ZAPS} from "@app/env"
import type {FeedContext} from "@app/feeds"
import {deriveEvent, deriveEventsForUrl} from "@app/repository"
import {makeContentPath} from "@app/routes"
import {pushModal} from "@app/modal"
@ -50,11 +51,20 @@
event: TrustedEvent
replyTo?: (event: TrustedEvent) => void
showPubkey?: boolean
context: FeedContext
canEdit: (event: TrustedEvent) => boolean
onEdit: (event: TrustedEvent) => void
}
const {url, event, replyTo = undefined, showPubkey = false, canEdit, onEdit}: Props = $props()
const {
url,
event,
replyTo = undefined,
showPubkey = false,
context,
canEdit,
onEdit,
}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags)
const today = formatTimestampAsDate(now())
@ -139,6 +149,7 @@
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-right"

View file

@ -1,9 +1,11 @@
<script lang="ts">
import {onDestroy} from "svelte"
import History from "@assets/icons/history.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import RecentItem from "@app/components/RecentItem.svelte"
import {makeFeedContext} from "@app/feeds"
import {deriveRecentActivity} from "@app/recent"
import {makeSpacePath} from "@app/routes"
@ -12,9 +14,12 @@
}
const {url}: Props = $props()
const context = makeFeedContext({relays: [url]})
const recentActivity = deriveRecentActivity(url)
const recentPath = makeSpacePath(url, "recent")
onDestroy(context.cleanup)
</script>
<div class="card flex flex-col gap-3">
@ -27,7 +32,7 @@
{:else}
<div class="flex flex-col gap-2">
{#each $recentActivity.slice(0, 3) as item (item.event.id)}
<RecentItem {url} {item} />
<RecentItem {url} {item} {context} />
{/each}
</div>
{/if}

View file

@ -5,6 +5,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
@ -15,9 +16,10 @@
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
context: FeedContext
}
const {url, event, showRoom, showActivity}: Props = $props()
const {url, event, showRoom, showActivity, context}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags)
const path = makeThreadPath(url, event.id)
@ -33,7 +35,13 @@
</Link>
{/if}
<ThunkStatusOrDeleted {event}>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}

View file

@ -6,15 +6,17 @@
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ThreadActions from "@app/components/ThreadActions.svelte"
import type {FeedContext} from "@app/feeds"
import RoomLink from "@app/components/RoomLink.svelte"
import {makeThreadPath} from "@app/routes"
type Props = {
url: string
event: TrustedEvent
context: FeedContext
}
const {url, event}: Props = $props()
const {url, event, context}: Props = $props()
const title = tagValue(tagSpec("title"), event.tags)
const h = tagValue(tagSpec("h"), event.tags)
@ -44,6 +46,6 @@
in <RoomLink {url} {h} />
{/if}
</span>
<ThreadActions showActivity {url} {event} />
<ThreadActions showActivity {url} {event} {context} />
</div>
</Link>

View file

@ -12,6 +12,7 @@
import NoteContent from "@app/components/NoteContent.svelte"
import Content from "@app/components/Content.svelte"
import CommentActions from "@app/components/CommentActions.svelte"
import type {FeedContext} from "@app/feeds"
import ThreadActions from "@app/components/ThreadActions.svelte"
import {handles, profiles} from "@app/core"
import {makeEventPermalink} from "@app/routes"
@ -23,9 +24,10 @@
event: TrustedEvent
threadPubkey: string
onReply: (event: TrustedEvent) => void
context: FeedContext
}
const {url, event, threadPubkey, onReply}: Props = $props()
const {url, event, threadPubkey, onReply, context}: Props = $props()
const profileDisplay = $profiles.display(event.pubkey, [url]).$
const handle = $handles.forPubkey(event.pubkey).$
@ -90,9 +92,9 @@
Reply
</Button>
{#if isComment}
<CommentActions segment="threads" {event} {url} />
<CommentActions segment="threads" {event} {url} {context} />
{:else}
<ThreadActions {event} {url} />
<ThreadActions {event} {url} {context} />
{/if}
</div>
</div>

View file

@ -36,6 +36,8 @@ export const makeDeleteFilter = (kinds: number[], extra: Filter = {}) => ({
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
export const REACTION_DISPLAY_KINDS = [REPORT, REACTION, ZAP_RECEIPT]
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
if (ENABLE_ZAPS) {

View file

@ -1,18 +1,179 @@
import {writable} from "svelte/store"
import {readable, writable} from "svelte/store"
import type {Readable} from "svelte/store"
import {batch, between, call, int, now, on, sortBy, uniqBy, MONTH, YEAR} from "@welshman/lib"
import {EVENT_TIME, getAddress, matchFilters, tagSpec, tagValue} from "@welshman/util"
import {
DELETE,
EVENT_TIME,
getAddress,
getReplyFilters,
hexTags,
matchFilters,
tagSpec,
tagValue,
tagValues,
} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import {mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
import {REACTION_DISPLAY_KINDS, REACTION_KINDS} from "@app/content"
import {app, network} from "@app/core"
import {getEventsForUrl} from "@app/repository"
const noEvents: TrustedEvent[] = []
export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}) => {
const {repository} = app.get()
const controller = new AbortController()
const targets = new Set<string>()
const eventsByTarget = new Map<string, TrustedEvent[]>()
const targetsByEventId = new Map<string, string[]>()
const subscribersByTarget = new Map<string, Set<(events: TrustedEvent[]) => void>>()
const addEvent = (event: TrustedEvent, touched: Set<string>) => {
// An event seen before its target was tracked stays unfiled, so that adding the target
// later can pick it up out of the repository
if (!targetsByEventId.has(event.id)) {
const eventTargets = tagValues(hexTags("e"), event.tags).filter(target => targets.has(target))
if (eventTargets.length > 0) {
targetsByEventId.set(event.id, eventTargets)
for (const target of eventTargets) {
eventsByTarget.set(target, [...(eventsByTarget.get(target) || []), event])
touched.add(target)
}
}
}
}
const removeEvent = (id: string, touched: Set<string>) => {
const eventTargets = targetsByEventId.get(id)
if (eventTargets) {
targetsByEventId.delete(id)
for (const target of eventTargets) {
const events = eventsByTarget.get(target)!.filter(event => event.id !== id)
if (events.length > 0) {
eventsByTarget.set(target, events)
} else {
eventsByTarget.delete(target)
}
touched.add(target)
}
}
}
const notify = (touched: Set<string>) => {
for (const target of touched) {
for (const subscriber of subscribersByTarget.get(target) || []) {
subscriber(eventsByTarget.get(target) || noEvents)
}
}
}
const loadContext = batch(100, async (events: TrustedEvent[]) => {
const touched = new Set<string>()
// What's already local — an earlier page, our own optimistic reactions — never comes
// through the update listener, so file it before asking the network for the rest
for (const event of repository.query(
getReplyFilters(events, {kinds: REACTION_DISPLAY_KINDS}),
)) {
addEvent(event, touched)
}
notify(touched)
const urls = await relays
const context = await network.get().load({
relays: urls,
signal: controller.signal,
filters: getReplyFilters(events, {kinds: REACTION_KINDS}),
})
if (context.length > 0) {
network.get().load({
relays: urls,
signal: controller.signal,
filters: getReplyFilters(context, {kinds: [DELETE]}),
})
}
})
const unsubscribe = on(
repository,
"update",
batch(150, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
const touched = new Set<string>()
for (const event of added) {
if (REACTION_DISPLAY_KINDS.includes(event.kind)) {
addEvent(event, touched)
}
}
for (const id of removed) {
removeEvent(id, touched)
}
notify(touched)
}),
)
// Track an event so its context gets requested with the rest of the batch
const add = (event: TrustedEvent) => {
if (!targets.has(event.id)) {
targets.add(event.id)
loadContext(event)
}
}
return {
add,
related: (event: TrustedEvent): Readable<TrustedEvent[]> => {
add(event)
return readable(eventsByTarget.get(event.id) || noEvents, set => {
let subscribers = subscribersByTarget.get(event.id)
if (!subscribers) {
subscribers = new Set()
subscribersByTarget.set(event.id, subscribers)
}
subscribers.add(set)
set(eventsByTarget.get(event.id) || noEvents)
return () => {
subscribers.delete(set)
if (subscribers.size === 0) {
subscribersByTarget.delete(event.id)
}
}
})
},
cleanup: () => {
controller.abort()
unsubscribe()
},
}
}
export type FeedContext = ReturnType<typeof makeFeedContext>
export const makeFeed = ({
relays,
filters,
element,
onEvent,
onBackwardExhausted,
onForwardExhausted,
at = now(),
@ -20,6 +181,7 @@ export const makeFeed = ({
relays: string[]
filters: Filter[]
element: HTMLElement
onEvent?: (event: TrustedEvent) => void
onBackwardExhausted?: () => void
onForwardExhausted?: () => void
at?: number
@ -64,6 +226,10 @@ export const makeFeed = ({
if (visible.length > 0) {
visible.sort((a, b) => a.created_at - b.created_at)
for (const event of visible) {
onEvent?.(event)
}
events.update($events => {
const merged: TrustedEvent[] = []
let i = 0
@ -223,11 +389,13 @@ export const makeCalendarFeed = ({
relays,
filters,
element,
onEvent,
onExhausted,
}: {
relays: string[]
filters: Filter[]
element: HTMLElement
onEvent?: (event: TrustedEvent) => void
onExhausted?: () => void
}) => {
const interval = int(5, MONTH)
@ -260,6 +428,7 @@ export const makeCalendarFeed = ({
for (const event of valid) {
seen.add(event.id)
onEvent?.(event)
}
valid.sort((a, b) => getStart(a) - getStart(b))

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {page} from "$app/stores"
@ -21,10 +21,13 @@
import {reader} from "@app/core"
import {decodeRelay} from "@app/relays"
import {makeCommentFilter} from "@app/content"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
let loading = $state(true)
let author: string | undefined = $state()
@ -60,6 +63,7 @@
const feed = makeFeed({
relays: [url],
element: element!,
onEvent: context.add,
filters: [{kinds: [LONG_FORM]}, makeCommentFilter([LONG_FORM])],
onBackwardExhausted: () => {
loading = false
@ -68,9 +72,7 @@
events = feed.events
return () => {
feed.cleanup()
}
return () => feed.cleanup()
})
</script>
@ -93,7 +95,7 @@
<div class="flex min-w-0 grow flex-col gap-2 sm:gap-4 p-2 sm:p-4 lg:pr-0">
{#each filtered as event (getAddress(event))}
<div in:fly>
<ArticleItem {url} {event} />
<ArticleItem {url} {event} {context} />
</div>
{/each}
<p class="flex h-10 items-center justify-center py-20">

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores"
import {formatTimestamp, sleep} from "@welshman/lib"
@ -13,14 +14,18 @@
import EventComments from "@app/components/EventComments.svelte"
import {reader} from "@app/core"
import {deriveEvent} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
const {relay, address} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const article = derived(event, $event => ($event ? reader(Article)($event) : undefined))
const back = () => history.back()
onDestroy(context.cleanup)
</script>
<SpaceBar {back}>
@ -46,10 +51,10 @@
<p class="text-lg opacity-75">{$article.summary()}</p>
{/if}
<ContentMarkdown event={$event} {url} />
<ArticleActions showRoom event={$event} {url} />
<ArticleActions showRoom event={$event} {url} {context} />
</div>
</NoteCard>
<EventComments event={$event} {url} />
<EventComments event={$event} {url} {context} />
{:else}
<div class="flex justify-center py-20">
{#await sleep(5000)}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import type {Readable} from "svelte/store"
import {readable} from "svelte/store"
import {page} from "$app/stores"
@ -20,15 +20,18 @@
import {pushModal} from "@app/modal"
import {decodeRelay} from "@app/relays"
import {makeCommentFilter} from "@app/content"
import {makeCalendarFeed} from "@app/feeds"
import {makeCalendarFeed, makeFeedContext} from "@app/feeds"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
const makeEvent = () => pushModal(CalendarEventCreate, {url})
const getStart = (event: TrustedEvent) => parseInt(tagValue(tagSpec("start"), event.tags) || "")
let element: HTMLElement | undefined = $state()
onDestroy(context.cleanup)
let loading = $state(true)
let events: Readable<TrustedEvent[]> = $state(readable([]))
@ -94,6 +97,7 @@
const feed = makeCalendarFeed({
relays: [url],
element: element!,
onEvent: context.add,
filters: [{kinds: [EVENT_TIME]}, makeCommentFilter([EVENT_TIME])],
onExhausted: () => {
loading = false
@ -102,9 +106,7 @@
events = feed.events
return () => {
feed.cleanup()
}
return () => feed.cleanup()
})
</script>
@ -136,7 +138,7 @@
{#if dateDisplay}
<Divider>{dateDisplay}</Divider>
{/if}
<CalendarEventItem {url} {event} />
<CalendarEventItem {url} {event} {context} />
</div>
{/each}
{#if loading}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores"
import {sortBy, sleep} from "@welshman/lib"
@ -23,10 +24,12 @@
import EventReply from "@app/components/EventReply.svelte"
import {network, reader} from "@app/core"
import {deriveEvent, deriveEvents} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
const {relay, address} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const timeEvent = derived(event, $event => ($event ? reader(TimeEvent)($event) : undefined))
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
@ -46,6 +49,8 @@
showAll = true
}
onDestroy(context.cleanup)
let showAll = $state(false)
let showReply = $state(false)
@ -81,7 +86,7 @@
</div>
</div>
<div class="flex w-full flex-col justify-end sm:flex-row">
<CalendarEventActions showRoom {url} event={$event} />
<CalendarEventActions showRoom {url} event={$event} {context} />
</div>
</div>
{#if !showAll && $replies.length > 4}
@ -96,7 +101,7 @@
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={reply} {url} />
<CommentActions segment="calendar" event={reply} {url} />
<CommentActions segment="calendar" event={reply} {url} {context} />
</div>
</NoteCard>
{/each}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {page} from "$app/stores"
@ -18,10 +18,13 @@
import ClassifiedCreate from "@app/components/ClassifiedCreate.svelte"
import {decodeRelay} from "@app/relays"
import {makeCommentFilter} from "@app/content"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
let loading = $state(true)
let element: HTMLElement | undefined = $state()
@ -48,6 +51,7 @@
const feed = makeFeed({
relays: [url],
element: element!,
onEvent: context.add,
filters: [{kinds: [CLASSIFIED]}, makeCommentFilter([CLASSIFIED])],
onBackwardExhausted: () => {
loading = false
@ -56,9 +60,7 @@
events = feed.events
return () => {
feed.cleanup()
}
return () => feed.cleanup()
})
</script>
@ -80,7 +82,7 @@
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<ClassifiedItem {url} event={$state.snapshot(event)} />
<ClassifiedItem {url} {context} event={$state.snapshot(event)} />
</div>
{/each}
<p class="flex h-10 items-center justify-center py-20">

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores"
import {sleep} from "@welshman/lib"
@ -20,10 +21,12 @@
import EventReply from "@app/components/EventReply.svelte"
import {network, reader} from "@app/core"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
const {relay, address} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const classified = derived(event, $event => ($event ? reader(Classified)($event) : undefined))
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
@ -43,6 +46,8 @@
showAll = true
}
onDestroy(context.cleanup)
let showAll = $state(false)
let showReply = $state(false)
@ -69,7 +74,7 @@
<NoteCard event={$event} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={$event} {url} />
<ClassifiedActions showRoom event={$event} {url} />
<ClassifiedActions showRoom event={$event} {url} {context} />
</div>
</NoteCard>
{#if !showAll && $replies.length > 4}
@ -84,7 +89,7 @@
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={reply} {url} />
<CommentActions segment="classifieds" event={reply} {url} />
<CommentActions segment="classifieds" event={reply} {url} {context} />
</div>
</NoteCard>
{/each}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {page} from "$app/stores"
@ -18,10 +18,13 @@
import GoalCreate from "@app/components/GoalCreate.svelte"
import {decodeRelay} from "@app/relays"
import {makeCommentFilter} from "@app/content"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
let loading = $state(true)
let element: HTMLElement | undefined = $state()
@ -48,6 +51,7 @@
const feed = makeFeed({
relays: [url],
element: element!,
onEvent: context.add,
filters: [{kinds: [ZAP_GOAL]}, makeCommentFilter([ZAP_GOAL])],
onBackwardExhausted: () => {
loading = false
@ -56,9 +60,7 @@
events = feed.events
return () => {
feed.cleanup()
}
return () => feed.cleanup()
})
</script>
@ -80,7 +82,7 @@
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<GoalItem {url} event={$state.snapshot(event)} />
<GoalItem {url} {context} event={$state.snapshot(event)} />
</div>
{/each}
<p class="flex h-10 items-center justify-center py-20">

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores"
import {sleep} from "@welshman/lib"
@ -20,10 +21,12 @@
import EventReply from "@app/components/EventReply.svelte"
import {network, reader} from "@app/core"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(id, [url])
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const replies = $derived(deriveEventsAsc(deriveEventsById(filters)))
@ -44,6 +47,8 @@
showAll = true
}
onDestroy(context.cleanup)
let showAll = $state(false)
let showReply = $state(false)
@ -70,7 +75,7 @@
<NoteCard event={$event} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={{...$event, content: summary}} {url} />
<GoalActions showRoom event={$event} {url} />
<GoalActions showRoom event={$event} {url} {context} />
</div>
</NoteCard>
{#if !showAll && $replies.length > 4}
@ -85,7 +90,7 @@
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={reply} {url} />
<CommentActions segment="goals" event={reply} {url} />
<CommentActions segment="goals" event={reply} {url} {context} />
</div>
</NoteCard>
{/each}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {page} from "$app/stores"
@ -18,10 +18,13 @@
import PollCreate from "@app/components/PollCreate.svelte"
import {decodeRelay} from "@app/relays"
import {makeCommentFilter} from "@app/content"
import {makeFeed} from "@app/feeds"
import {makeFeed, makeFeedContext} from "@app/feeds"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
let loading = $state(true)
let element: HTMLElement | undefined = $state()
@ -48,6 +51,7 @@
const feed = makeFeed({
relays: [url],
element: element!,
onEvent: context.add,
filters: [{kinds: [POLL]}, makeCommentFilter([POLL])],
onBackwardExhausted: () => {
loading = false
@ -56,9 +60,7 @@
events = feed.events
return () => {
feed.cleanup()
}
return () => feed.cleanup()
})
</script>
@ -80,7 +82,7 @@
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#each items as event (event.id)}
<div in:fly>
<PollItem {url} event={$state.snapshot(event)} />
<PollItem {url} {context} event={$state.snapshot(event)} />
</div>
{/each}
<p class="flex h-10 items-center justify-center py-20">

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {page} from "$app/stores"
import {sleep} from "@welshman/lib"
import type {MakeNonOptional} from "@welshman/lib"
@ -17,10 +17,12 @@
import EventReply from "@app/components/EventReply.svelte"
import {network} from "@app/core"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(id, [url])
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const comments = $derived(deriveEventsAsc(deriveEventsById(filters)))
@ -39,6 +41,8 @@
showAll = true
}
onDestroy(context.cleanup)
let showAll = $state(false)
let showReply = $state(false)
@ -82,7 +86,7 @@
<NoteCard event={$event} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12 flex flex-col gap-3">
<NoteContent showEntire event={$event} {url} />
<CommentActions segment="polls" showActivity {url} event={$event} />
<CommentActions segment="polls" showActivity {url} event={$event} {context} />
</div>
</NoteCard>
{#if !showAll && $comments.length > 4}
@ -97,7 +101,7 @@
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={reply} {url} />
<CommentActions segment="polls" event={reply} {url} />
<CommentActions segment="polls" event={reply} {url} {context} />
</div>
</NoteCard>
{/each}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {onDestroy, onMount} from "svelte"
import {page} from "$app/stores"
import History from "@assets/icons/history.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@ -9,12 +9,16 @@
import PageContent from "@lib/components/PageContent.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import RecentItem from "@app/components/RecentItem.svelte"
import {makeFeedContext} from "@app/feeds"
import SpaceSearch from "@app/components/SpaceSearch.svelte"
import {decodeRelay} from "@app/relays"
import {deriveRecentActivity} from "@app/recent"
import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay!)
const context = makeFeedContext({relays: [url]})
onDestroy(context.cleanup)
const recentActivity = deriveRecentActivity(url)
@ -57,7 +61,7 @@
<p class="flex flex-col items-center py-20 text-center">No recent activity found!</p>
{:else}
{#each $recentActivity.slice(0, limit) as item (item.event.id)}
<RecentItem {url} {item} />
<RecentItem {url} {item} {context} />
{/each}
{/if}
</PageContent>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import {page} from "$app/stores"
import {goto} from "$app/navigation"
@ -20,6 +21,7 @@
import RoomName from "@app/components/RoomName.svelte"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import {makeSpacePath, scrollToEvent} from "@app/routes"
@ -28,6 +30,8 @@
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
const url = decodeRelay(relay)
const event = deriveEvent(id, [url])
// Rows register themselves with `related`, so the whole page's reactions load in one batch
const context = makeFeedContext({relays: [url]})
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const replies = $derived(deriveEventsAsc(deriveEventsById(filters)))
@ -96,6 +100,8 @@
}
}
onDestroy(context.cleanup)
let showReply = $state(false)
let replyTo: TrustedEvent | undefined = $state()
@ -172,7 +178,7 @@
{#if $event}
<div class="bg-surface border-y" style="border-color: var(--line)">
{#each pagePosts as post (post.id)}
<ThreadPost {url} event={post} threadPubkey={$event.pubkey} onReply={openReply} />
<ThreadPost {url} {context} event={post} threadPubkey={$event.pubkey} onReply={openReply} />
{/each}
</div>
{#if pageCount > 1}