Read thread, classified, article and goal titles, rooms and reports through their domain readers

This commit is contained in:
Jon Staab 2026-09-25 21:15:39 -07:00
parent 945a9649e6
commit 66af2a201e
13 changed files with 51 additions and 52 deletions

View file

@ -300,8 +300,7 @@ These predate the rule or are waiting on welshman. Don't copy them; fix one when
relay lists in `SignUp.svelte` (`RelayList`, `MessagingRelayList`); deletes in
`ProfileDelete.svelte` and the push adapter (`Delete`); the vanish request as a literal `62`
(`VANISH`).
- **Getter exists but bypassed:** titles in `src/app/title.ts`, `NoteContentThread` and the
threads pages; calendar `start`/`end` in `src/app/feeds.ts` and the calendar page; poll `response`
- **Getter exists but bypassed:** calendar `start`/`end` in `src/app/feeds.ts` and the calendar page; poll `response`
tags in `PollVotes`; `p` tags of `RELAY_ADD_MEMBER`, `ROOM_ADD_MEMBER` and
`ROOM_CREATE_PERMISSION` (`SpaceMembersSummary`, `RoomItemAddMember`, `src/app/management.ts`);
comment `E`/`A` tags in the list pages and `src/app/classifieds.ts`; `imeta` unpacking in

View file

@ -1,10 +1,12 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {getAddress, tagValue, tagSpec} from "@welshman/util"
import {getAddress} from "@welshman/util"
import {TimeEvent} from "@welshman/domain"
import Link from "@lib/components/Link.svelte"
import Cv from "@lib/components/Cv.svelte"
import ContentActions from "@app/components/ContentActions.svelte"
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
import {reader} from "@app/core"
import type {FeedContext} from "@app/feeds"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
@ -21,7 +23,7 @@
const {url, event, context}: Props = $props()
const path = $derived(makeCalendarPath(url, getAddress(event)))
const h = $derived(tagValue(tagSpec("h"), event.tags))
const h = $derived(reader(TimeEvent)(event).room())
</script>
<Cv tag={Link} class="relative flex flex-col gap-3 card card-interactive w-full" href={path}>

View file

@ -1,11 +1,12 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {tagSpec, tagValue} from "@welshman/util"
import {Thread} from "@welshman/domain"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {reader} from "@app/core"
const props: ComponentProps<typeof ContentMinimal> = $props()
const title = tagValue(tagSpec("title"), props.event.tags)
const title = reader(Thread)(props.event).title()
</script>
{#if title}

View file

@ -1,11 +1,12 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {tagSpec, tagValue} from "@welshman/util"
import {Thread} from "@welshman/domain"
import Content from "@app/components/Content.svelte"
import {reader} from "@app/core"
const props: ComponentProps<typeof Content> = $props()
const title = tagValue(tagSpec("title"), props.event.tags)
const title = reader(Thread)(props.event).title()
</script>
<div class="flex flex-col gap-2">

View file

@ -1,10 +1,11 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util"
import {Poll} from "@welshman/domain"
import Link from "@lib/components/Link.svelte"
import Cv from "@lib/components/Cv.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import ContentActions from "@app/components/ContentActions.svelte"
import {reader} from "@app/core"
import type {FeedContext} from "@app/feeds"
import RoomLink from "@app/components/RoomLink.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
@ -20,7 +21,7 @@
const {url, event, context}: Props = $props()
const path = makePollPath(url, event.id)
const h = tagValue(tagSpec("h"), event.tags)
const h = reader(Poll)(event).room()
</script>
<Cv tag={Link} class="relative flex flex-col gap-2 card card-interactive w-full" href={path}>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {matchTag, tagSpec} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Report} from "@welshman/domain"
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@ -9,7 +9,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import {app, deletes, profiles, relayManagement, relays, user} from "@app/core"
import {app, deletes, profiles, reader, relayManagement, relays, user} from "@app/core"
import {deriveSpacePermissions} from "@app/management"
import {pushToast} from "@app/toast"
import {pushModal} from "@app/modal"
@ -24,8 +24,10 @@
const {url, event, onResolved, onClick}: Props = $props()
const permissions = deriveSpacePermissions(url)
const etag = matchTag(tagSpec("e"), event.tags)
const ptag = matchTag(tagSpec("p"), event.tags)
const report = reader(Report)(event)
const reportedId = report.eventId()
const reportedPubkey = report.pubkey()
const reason = report.reason() ?? ""
const deleteReport = async () => {
const protect = await $relays.hasNip(url, 70)
@ -48,20 +50,18 @@
}
const banContent = () => {
const [_, id, reason = ""] = etag!
pushModal(Confirm, {
title: `Remove Content`,
message: `Are you sure you want to delete this content from the space?`,
confirm: async () => {
const {error} = await $relayManagement.forUrl(url).banEvent(id, reason)
const {error} = await $relayManagement.forUrl(url).banEvent(reportedId!, reason)
if (error) {
pushToast({theme: "error", message: error})
} else {
pushToast({message: "Content has successfully been deleted!"})
$app.repository.removeEvent(event.id)
$app.repository.removeEvent(id)
$app.repository.removeEvent(reportedId!)
history.back()
setTimeout(() => onResolved?.())
}
@ -70,13 +70,11 @@
}
const banMember = () => {
const [_, reported, reason = ""] = ptag!
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${$profiles.display(reported, [url]).get()} from the space?`,
message: `Are you sure you want to ban @${$profiles.display(reportedPubkey!, [url]).get()} from the space?`,
confirm: async () => {
const {error} = await $relayManagement.forUrl(url).banPubkey(reported, reason)
const {error} = await $relayManagement.forUrl(url).banPubkey(reportedPubkey!, reason)
if (error) {
pushToast({theme: "error", message: error})
@ -113,7 +111,7 @@
Dismiss Report
</Button>
</li>
{#if etag}
{#if reportedId}
<li>
<Button class="text-error" onclick={banContent}>
<Icon icon={TrashBin2} />
@ -122,7 +120,7 @@
</li>
{/if}
{/if}
{#if ptag && $permissions.ban}
{#if reportedPubkey && $permissions.ban}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />

View file

@ -3,15 +3,8 @@
import {readable} from "svelte/store"
import {now, uniq, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {
MESSAGE,
THREAD,
getCommentFiltersForParent,
getIdOrAddress,
tagSpec,
tagValue,
} from "@welshman/util"
import {getContentQuote} from "@welshman/domain"
import {MESSAGE, THREAD, getCommentFiltersForParent, getIdOrAddress} from "@welshman/util"
import {Thread, getContentQuote} from "@welshman/domain"
import {isMobile} from "@lib/html"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
@ -32,7 +25,7 @@
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {events, profiles, thunks, user} from "@app/core"
import {events, profiles, reader, thunks, user} from "@app/core"
import {noThunks, thunksByEventId} from "@app/thunks"
import {colorFor} from "@app/theme"
import {ENABLE_ZAPS} from "@app/env"
@ -154,7 +147,7 @@
class="button button-xs button-neutral gap-1 rounded-full">
<Icon icon={NotesMinimalistic} />
<span class="max-w-48 truncate">
{tagValue(tagSpec("title"), thread.tags) || "Thread"}
{reader(Thread)(thread).title() || "Thread"}
</span>
</Link>
{/each}

View file

@ -1,12 +1,12 @@
<script lang="ts">
import {tagSpec, tagValue} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {RoomJoin} from "@welshman/domain"
import Card from "@lib/components/Card.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {app, relayManagement} from "@app/core"
import {app, reader, relayManagement} from "@app/core"
import {deriveSpacePermissions} from "@app/management"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
@ -20,7 +20,7 @@
const {url, event, onResolved}: Props = $props()
const h = tagValue(tagSpec("h"), event.tags) || ""
const h = reader(RoomJoin)(event).room() || ""
const permissions = deriveSpacePermissions(url)

View file

@ -3,12 +3,14 @@
import {derived} from "svelte/store"
import {filter, formatTimestamp, max, spec} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, tagSpec, tagValue} from "@welshman/util"
import {COMMENT} from "@welshman/util"
import {Thread} from "@welshman/domain"
import Link from "@lib/components/Link.svelte"
import Cv from "@lib/components/Cv.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import UnreadDot from "@app/components/UnreadDot.svelte"
import {reader} from "@app/core"
import type {FeedContext} from "@app/feeds"
import {makeThreadPath} from "@app/routes"
@ -25,7 +27,7 @@
const replies = derived(related, $related => filter(spec({kind: COMMENT}), $related))
const replyCount = $derived($replies.length)
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
const title = tagValue(tagSpec("title"), event.tags)
const title = reader(Thread)(event).title()
const path = makeThreadPath(url, event.id)
const onClick = () => navigate(path)
</script>

View file

@ -1,9 +1,10 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util"
import {Thread} from "@welshman/domain"
import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {reader} from "@app/core"
type Props = {
url: string
@ -13,7 +14,7 @@
const {url, event, onClick}: Props = $props()
const title = tagValue(tagSpec("title"), event.tags)
const title = reader(Thread)(event).title()
</script>
<Button

View file

@ -37,9 +37,9 @@ export const AUDIO_CONTENT_TYPES = [
]
export const getUrlTags = (url: string, event: TrustedEvent) => {
// An imeta tag packs its own tags into space-separated values, so unpack them.
// An imeta entry is "key value" and the value may contain spaces, so split at the first one only.
const imetas = matchTags(tagSpec("imeta"), event.tags).map(([, ...values]: string[]) =>
values.map(value => value.split(" ")),
values.map(value => value.split(/ (.*)/s, 2)),
)
const imeta = imetas.find(meta => tagValue(tagSpec("url"), meta) === url)

View file

@ -1,10 +1,9 @@
import {identity, uniq} from "@welshman/lib"
import {tagSpec, tagValue} from "@welshman/util"
import {displayPubkey} from "@welshman/domain"
import {Article, Classified, Thread, ZapGoal, displayPubkey} from "@welshman/domain"
import {makeRoomKey} from "@welshman/app"
import {decodePubkey} from "@lib/util"
import {getEventTitle} from "@app/calendar"
import {app, profiles, rooms} from "@app/core"
import {app, profiles, reader, rooms} from "@app/core"
import {PLATFORM_NAME} from "@app/env"
import {decodeRelay} from "@app/relays"
@ -134,7 +133,7 @@ export const getPageTitle = ({page, pubkey}: PageTitleContext) => {
const event = getEventForTitle(routeId, page.params)
if (routeId === "/spaces/[relay]/threads/[id]") {
return makeTitle(tagValue(tagSpec("title"), event?.tags || []) || "Thread")
return makeTitle((event && reader(Thread)(event).title()) || "Thread")
}
if (routeId === "/spaces/[relay]/calendar/[address]") {
@ -142,15 +141,15 @@ export const getPageTitle = ({page, pubkey}: PageTitleContext) => {
}
if (routeId === "/spaces/[relay]/classifieds/[address]") {
return makeTitle(tagValue(tagSpec("title"), event?.tags || []) || "Listing")
return makeTitle((event && reader(Classified)(event).title()) || "Listing")
}
if (routeId === "/spaces/[relay]/articles/[address]") {
return makeTitle(tagValue(tagSpec("title"), event?.tags || []) || "Article")
return makeTitle((event && reader(Article)(event).title()) || "Article")
}
if (routeId === "/spaces/[relay]/goals/[id]") {
return makeTitle(event?.content || tagValue(tagSpec("summary"), event?.tags || []) || "Goal")
return makeTitle(event?.content || (event && reader(ZapGoal)(event).summary()) || "Goal")
}
return makeTitle()

View file

@ -6,12 +6,14 @@
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {THREAD, tagValue, tagSpec} from "@welshman/util"
import {Thread} from "@welshman/domain"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import ThreadBoard from "@app/components/ThreadBoard.svelte"
import {reader} from "@app/core"
import {decodeRelay} from "@app/relays"
import {displayRoom} from "@app/rooms"
import {makeCommentFilter} from "@app/content"
@ -46,7 +48,7 @@
const items = sortBy(e => -max([...(scores.get(e.id) || []), e.created_at]), threads)
const grouped = groupBy(e => tagValue(tagSpec("h"), e.tags) || "", items)
const grouped = groupBy(e => reader(Thread)(e).room() || "", items)
const byRoom = new Map<string, TrustedEvent[]>([["", []], ...grouped])
const roomName = (h: string) => (h ? displayRoom(url, h) : "general").toLowerCase()
const boards = sortBy(([h]) => roomName(h), Array.from(byRoom.entries()))