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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@
import RoomName from "@app/components/RoomName.svelte" import RoomName from "@app/components/RoomName.svelte"
import {publishReaction, retractReaction} from "@app/reactions" import {publishReaction, retractReaction} from "@app/reactions"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import type {FeedContext} from "@app/feeds"
import ClassifiedStatus from "@app/components/ClassifiedStatus.svelte" import ClassifiedStatus from "@app/components/ClassifiedStatus.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte" import EventActivity from "@app/components/EventActivity.svelte"
@ -25,9 +26,10 @@
event: TrustedEvent event: TrustedEvent
showRoom?: boolean showRoom?: boolean
showActivity?: 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 // 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. // recomputed rather than captured when the component was created.
@ -60,7 +62,13 @@
{#snippet status()} {#snippet status()}
<ClassifiedStatus {event} /> <ClassifiedStatus {event} />
{/snippet} {/snippet}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" /> <ReactionSummary
{url}
{event}
{context}
{deleteReaction}
{createReaction}
reactionClass="tip-left" />
{#if showActivity} {#if showActivity}
<EventActivity {url} {path} {event} /> <EventActivity {url} {path} {event} />
{/if} {/if}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,8 @@ export const makeDeleteFilter = (kinds: number[], extra: Filter = {}) => ({
export const REPOST_KINDS = [REPOST, GENERIC_REPOST] export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
export const REACTION_DISPLAY_KINDS = [REPORT, REACTION, ZAP_RECEIPT]
export const REACTION_KINDS = [REPORT, DELETE, REACTION] export const REACTION_KINDS = [REPORT, DELETE, REACTION]
if (ENABLE_ZAPS) { 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 {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 type {Filter, TrustedEvent} from "@welshman/util"
import {mergeRepositoryUpdates} from "@welshman/net" import {mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import {REACTION_DISPLAY_KINDS, REACTION_KINDS} from "@app/content"
import {app, network} from "@app/core" import {app, network} from "@app/core"
import {getEventsForUrl} from "@app/repository" 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 = ({ export const makeFeed = ({
relays, relays,
filters, filters,
element, element,
onEvent,
onBackwardExhausted, onBackwardExhausted,
onForwardExhausted, onForwardExhausted,
at = now(), at = now(),
@ -20,6 +181,7 @@ export const makeFeed = ({
relays: string[] relays: string[]
filters: Filter[] filters: Filter[]
element: HTMLElement element: HTMLElement
onEvent?: (event: TrustedEvent) => void
onBackwardExhausted?: () => void onBackwardExhausted?: () => void
onForwardExhausted?: () => void onForwardExhausted?: () => void
at?: number at?: number
@ -64,6 +226,10 @@ export const makeFeed = ({
if (visible.length > 0) { if (visible.length > 0) {
visible.sort((a, b) => a.created_at - b.created_at) visible.sort((a, b) => a.created_at - b.created_at)
for (const event of visible) {
onEvent?.(event)
}
events.update($events => { events.update($events => {
const merged: TrustedEvent[] = [] const merged: TrustedEvent[] = []
let i = 0 let i = 0
@ -223,11 +389,13 @@ export const makeCalendarFeed = ({
relays, relays,
filters, filters,
element, element,
onEvent,
onExhausted, onExhausted,
}: { }: {
relays: string[] relays: string[]
filters: Filter[] filters: Filter[]
element: HTMLElement element: HTMLElement
onEvent?: (event: TrustedEvent) => void
onExhausted?: () => void onExhausted?: () => void
}) => { }) => {
const interval = int(5, MONTH) const interval = int(5, MONTH)
@ -260,6 +428,7 @@ export const makeCalendarFeed = ({
for (const event of valid) { for (const event of valid) {
seen.add(event.id) seen.add(event.id)
onEvent?.(event)
} }
valid.sort((a, b) => getStart(a) - getStart(b)) valid.sort((a, b) => getStart(a) - getStart(b))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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