Replace the per-kind action bars with one ContentActions and give the article page its own ArticleActionBar
This commit is contained in:
parent
c710a15879
commit
9a49aa6638
26 changed files with 159 additions and 398 deletions
|
|
@ -319,7 +319,7 @@ export const getStatus = (event: TrustedEvent) => reader(Classified)(event).stat
|
|||
**Components.** `ClassifiedForm` builds and publishes the event, and `ClassifiedCreate` and
|
||||
`ClassifiedEdit` wrap it, supplying only the header. The list page and `ComposeMenu` open
|
||||
`ClassifiedCreate` as a modal. From a room, `ComposeMenu` sets `shareToChat`, which also quotes the
|
||||
new listing into the room. `ClassifiedActions` opens `ClassifiedEdit`. `ClassifiedItem` is the
|
||||
new listing into the room. Its `ContentActions` row opens `ClassifiedEdit` through `editForm`. `ClassifiedItem` is the
|
||||
card, and `NoteContentClassified` renders a listing wherever `NoteContent` is used.
|
||||
|
||||
**Routes.** `src/routes/spaces/[relay]/classifieds/+page.svelte` loads listings and their comments
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
|
|||
|
||||
| Feature | Constant (kind) | Factory | Module | Where |
|
||||
|---|---|---|---|---|
|
||||
| Reactions | `REACTION` (7) | `Reaction`, through the `Reactions` plugin | `src/app/reactions.ts` | `RoomItem`, `EventReactButtons`, the `*Actions` components |
|
||||
| Reactions | `REACTION` (7) | `Reaction`, through the `Reactions` plugin | `src/app/reactions.ts` | `RoomItem`, `EventReactButtons`, `ContentActions`, `CommentActions`, `ArticleActionBar` |
|
||||
| Zaps | `ZAP_REQUEST` (9734), `ZAP_RECEIPT` (9735) | `ZapRequest`; receipts checked by `Zappers.validZapReceipts` | `src/app/lightning.ts` (wallets) | `Zap`, `ZapButton`, `GoalSummary`; off on iOS |
|
||||
| Reports | `REPORT` (1984) | `Report` | `src/app/actionItems.ts` | `Report`, `ReportMenuList` |
|
||||
| Deletes | `DELETE` (5) | `Delete`, through the `Deletes` plugin | none | `EventDeleteConfirm`, `ReportMenuList` |
|
||||
|
|
|
|||
|
|
@ -238,8 +238,7 @@ New components go in the flat folder.
|
|||
## Naming
|
||||
|
||||
Names are `<Entity><Qualifier>`: the prefix says what it's about, the suffix what it does, which
|
||||
keeps families adjacent in a flat listing. `ClassifiedActions`, `ClassifiedCreate`,
|
||||
`ClassifiedEdit`, `ClassifiedForm`, `ClassifiedItem`, `ClassifiedStatus`.
|
||||
keeps families adjacent in a flat listing. `ClassifiedCreate`, `ClassifiedEdit`, `ClassifiedForm`, `ClassifiedItem`, `ClassifiedStatus`.
|
||||
|
||||
Name a modal for what it does (`SpaceJoin`, `ClassifiedCreate`, `EventDeleteConfirm`); only a
|
||||
handful carry a `Modal` or `Dialog` suffix. The suffix vocabulary, the prefix families, and the
|
||||
|
|
@ -262,7 +261,7 @@ domain reader: `RoleEdit` takes `role: RelayRoleReader`.
|
|||
|
||||
Identifier props are read once, `const room = $rooms.forRoom(url, h)`, since pages remount on a
|
||||
param change and lists key by id. A prop that does change while mounted needs `$derived`:
|
||||
`ClassifiedActions` reads its event that way, because editing a listing hands it a new version.
|
||||
`ContentActions` reads its event that way, because editing a listing hands it a new version.
|
||||
|
||||
### The rest of the vocabulary
|
||||
|
||||
|
|
@ -374,7 +373,7 @@ onMount(() => {
|
|||
|
||||
The feed is built in `onMount` because the loader needs the bound scroll element from
|
||||
`<PageContent bind:element>`. `context.add` batches the reactions, comments and deletions for
|
||||
every event the feed yields, rows get the same `context`, and their `*Actions` read
|
||||
every event the feed yields, rows get the same `context`, and their action rows read
|
||||
`context.related(event)` and `context.deleted(event)`. Close the list with `<Spinner {loading}>`
|
||||
and an `{#if}` chain over loading, empty and exhausted. Calendars use
|
||||
`makeCalendarFeed`, which pages by date tag rather than `created_at`.
|
||||
|
|
@ -417,8 +416,8 @@ return a `Command` as well: `$deletes.deleteEvent(event, w => w.setProtected(pro
|
|||
|
||||
The hosting backend isn't nostr. Toast a readable message, and `console.error` anything that
|
||||
isn't a `HostingError`, as `HomeHosting` and `hosting/CustomDomainModal` do. Publishes are
|
||||
optimistic, so rows show progress in place: the seven per-kind `*Actions` components wrap their
|
||||
contents in `ThunkStatusOrDeleted`, and chat pushes a `ThunkToast`.
|
||||
optimistic, so rows show progress in place: `ContentActions`, `CommentActions` and `ArticleActionBar`
|
||||
wrap their contents in `ThunkStatusOrDeleted`, and chat pushes a `ThunkToast`.
|
||||
|
||||
## Forms
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ PascalCase. This is what the qualifiers mean, drawn from the ~320 components the
|
|||
| Suffix | Means | Examples |
|
||||
|---|---|---|
|
||||
| `Item` | one row or card in a list | `ClassifiedItem`, `NoteItem`, `PeopleItem`, `ReportItem`, `RoomItem` (a message in a room), `SpaceMenuRoomItem` |
|
||||
| `Actions` | the footer row under an event: reactions, status, overflow menu | `ClassifiedActions`, `ThreadActions`, `GoalActions`, `EventActions` |
|
||||
| `Actions` | the footer row under an event: reactions, status, overflow menu | `ContentActions`, `CommentActions`, `EventActions` |
|
||||
| `Menu` | the contents of a popover, taking an `onClick` that closes it | `EventMenu`, `ChatMenu`, `RoomItemMenu`, `SpaceMemberMenu` |
|
||||
| `MenuList` | the popover contents when `*Menu` is the trigger instead | `ProfileMenu` → `ProfileMenuList`, `ReportMenu` → `ReportMenuList` |
|
||||
| `Mobile` | the same actions as a modal, pushed on tap instead of hovered | `RoomItemMenuMobile`, `ChatMessageMenuMobile`, `SpaceMenuMobile` |
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ test("US-040 react to a post with an emoji", async ({seed, as}) => {
|
|||
await openArticle(page, "Tending the Garden")
|
||||
|
||||
// An article's own reactions live in the action bar under it rather than on the article itself.
|
||||
const articleActions = page.locator('[data-component="ArticleActions"]')
|
||||
const articleActions = page.locator('[data-component="ArticleActionBar"]')
|
||||
const commentCard = comment(page, "A note about the soil")
|
||||
|
||||
await expect(articleActions).toBeVisible()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const commentCard = (page: Page, text: string) =>
|
|||
page.locator('[data-component="Comment"]').filter({hasText: text})
|
||||
|
||||
// The action bar under an article on its own page, which is where that article's status shows.
|
||||
const articleActions = (page: Page) => page.locator('[data-component="ArticleActions"]')
|
||||
const articleActions = (page: Page) => page.locator('[data-component="ArticleActionBar"]')
|
||||
|
||||
// Which relays a given event was sent to, oldest first — one entry per attempt.
|
||||
const publishedTo = (page: Page, id: string) =>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,29 @@
|
|||
<script lang="ts">
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import EventActionBar from "@app/components/EventActionBar.svelte"
|
||||
import {events} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
}
|
||||
|
||||
const {url, event, showRoom, showActivity, context}: Props = $props()
|
||||
const {url, event, context}: Props = $props()
|
||||
|
||||
const deleted = $derived($events.isDeleted(event).$)
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url})
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<div data-component="ArticleActionBar" class="flex w-full min-w-0 flex-col gap-3">
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
|
|
@ -40,10 +31,13 @@
|
|||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
reactionClass="tip-top" />
|
||||
</ThunkStatusOrDeleted>
|
||||
{#if !$deleted}
|
||||
<EventActionBar {url} {event} noun="Article">
|
||||
{#snippet leading()}
|
||||
<EventActivity {event} {context} size="sm" hideLastActive />
|
||||
{/snippet}
|
||||
</EventActionBar>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
<script lang="ts">
|
||||
import {uniq} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {Article} from "@welshman/domain"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import EventActionBar from "@app/components/EventActionBar.svelte"
|
||||
import {events, reader} from "@app/core"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
// The article's own page, where this is the primary action bar rather than a card footer.
|
||||
detail?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showRoom, showActivity, context, detail}: Props = $props()
|
||||
|
||||
const article = $derived(reader(Article)(event))
|
||||
const h = $derived(article.room())
|
||||
const topics = $derived(article.topics())
|
||||
const deleted = $derived($events.isDeleted(event).$)
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
</script>
|
||||
|
||||
{#if detail}
|
||||
<div data-component="ArticleActions" class="flex w-full min-w-0 flex-col gap-3">
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-top" />
|
||||
</ThunkStatusOrDeleted>
|
||||
{#if !$deleted}
|
||||
<EventActionBar {url} {event} noun="Article">
|
||||
{#snippet leading()}
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} size="sm" hideLastActive />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActionBar>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div data-component="ArticleActions" class="flex grow flex-wrap items-center justify-end gap-2">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-wrap gap-2">
|
||||
{#each uniq(topics) as topic (topic)}
|
||||
<button type="button" class="button button-xs rounded-full font-normal">
|
||||
#{topic}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
{#if !$deleted}
|
||||
<EventActions {url} {event} noun="Article" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
import {formatTimestamp, uniq} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {Article} from "@welshman/domain"
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
import Cv from "@lib/components/Cv.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ArticleActions from "@app/components/ArticleActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {reader} from "@app/core"
|
||||
|
|
@ -26,8 +26,15 @@
|
|||
const title = $derived(article.title())
|
||||
const summary = $derived(article.summary())
|
||||
const image = $derived(article.image())
|
||||
const topics = $derived(uniq(article.topics()))
|
||||
</script>
|
||||
|
||||
{#snippet topicLabels()}
|
||||
{#each topics as topic (topic)}
|
||||
<span class="button button-xs rounded-full font-normal">#{topic}</span>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<Cv data-component="ArticleItem" class="relative w-full card card-interactive">
|
||||
<!-- An overlay rather than a wrapper: the card carries a profile button and the room and action
|
||||
links, and none of those may sit inside an anchor. -->
|
||||
|
|
@ -55,7 +62,14 @@
|
|||
<ProfileLink pubkey={event.pubkey} {url} />
|
||||
</span>
|
||||
<div class="pointer-events-auto">
|
||||
<ArticleActions showRoom showActivity {url} {event} {context} />
|
||||
<ContentActions
|
||||
showRoom
|
||||
showActivity
|
||||
noun="Article"
|
||||
labels={topics.length > 0 ? topicLabels : undefined}
|
||||
{url}
|
||||
{event}
|
||||
{context} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
import {getAddress, tagValue, tagSpec} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Cv from "@lib/components/Cv.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
|
@ -33,6 +34,12 @@
|
|||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<CalendarEventActions showActivity {url} {event} {context} />
|
||||
<ContentActions
|
||||
showActivity
|
||||
noun="Event"
|
||||
editForm={CalendarEventEdit}
|
||||
{url}
|
||||
{event}
|
||||
{context} />
|
||||
</div>
|
||||
</Cv>
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
<script lang="ts">
|
||||
import {uniq} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {Classified} from "@welshman/domain"
|
||||
import {goto} from "$app/navigation"
|
||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
import {normalizeTopic} from "@lib/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ClassifiedStatus from "@app/components/ClassifiedStatus.svelte"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import ClassifiedEdit from "@app/components/ClassifiedEdit.svelte"
|
||||
import {reader, user} from "@app/core"
|
||||
import {makeClassifiedPath, makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
// The grid card lays these out itself, so it opts out of both.
|
||||
showTopics?: boolean
|
||||
showStatus?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
showRoom,
|
||||
showActivity,
|
||||
context,
|
||||
showTopics = true,
|
||||
showStatus = true,
|
||||
}: Props = $props()
|
||||
|
||||
// Editing hands this a new version of the event, so every value is recomputed rather than captured.
|
||||
const classified = $derived(reader(Classified)(event))
|
||||
const h = $derived(classified.room())
|
||||
const topics = $derived(classified.topics() ?? [])
|
||||
|
||||
const editClassified = () => pushModal(ClassifiedEdit, {url, event})
|
||||
|
||||
const filterByTopic = (topic: string) => (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
goto(`${makeClassifiedPath(url)}?topic=${encodeURIComponent(normalizeTopic(topic))}`)
|
||||
}
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-wrap items-center justify-end gap-2">
|
||||
{#if (h && showRoom) || (showTopics && uniq(topics).length > 0)}
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
{#if showTopics}
|
||||
{#each uniq(topics) as topic (topic)}
|
||||
<button
|
||||
type="button"
|
||||
class="badge badge-neutral badge-sm cursor-pointer font-normal"
|
||||
onclick={filterByTopic(topic)}>
|
||||
#{normalizeTopic(topic)}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<span class="h-6 w-px shrink-0 bg-line"></span>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
{#snippet status()}
|
||||
{#if showStatus}
|
||||
<ClassifiedStatus {event} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Listing">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editClassified}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Listing
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
import CurrencySymbol from "@lib/components/CurrencySymbol.svelte"
|
||||
import {reader} from "@app/core"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import ClassifiedEdit from "@app/components/ClassifiedEdit.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import {makeClassifiedPath} from "@app/routes"
|
||||
|
|
@ -99,6 +100,6 @@
|
|||
</span>
|
||||
<span class="truncate">{formatTimestampRelative(event.created_at)}</span>
|
||||
</span>
|
||||
<ClassifiedActions {url} {event} {context} showTopics={false} showStatus={false} />
|
||||
<ContentActions noun="Listing" editForm={ClassifiedEdit} {url} {event} {context} />
|
||||
</div>
|
||||
</Cv>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
|
|
@ -17,11 +16,9 @@
|
|||
|
||||
const {url, event, showActivity = false, context}: Props = $props()
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url})
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type {Component, Snippet} from "svelte"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
|
|
@ -6,60 +7,84 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import {user} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type EditForm = Component<{url: string; event: TrustedEvent}>
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
context: FeedContext
|
||||
noun: string
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
hideZap?: boolean
|
||||
editForm?: EditForm
|
||||
labels?: Snippet
|
||||
status?: Snippet
|
||||
}
|
||||
|
||||
const {url, event, showRoom, showActivity, context}: Props = $props()
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
context,
|
||||
noun,
|
||||
showRoom,
|
||||
showActivity,
|
||||
hideZap,
|
||||
editForm,
|
||||
labels,
|
||||
status,
|
||||
}: Props = $props()
|
||||
|
||||
const h = $derived(tagValue(tagSpec("h"), event.tags))
|
||||
|
||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||
const edit = (form: EditForm) => () => pushModal(form, {url, event})
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url})
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
<div class="flex grow flex-wrap items-center justify-end gap-2">
|
||||
{#if (h && showRoom) || labels}
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
{@render labels?.()}
|
||||
</div>
|
||||
<span class="h-6 w-px shrink-0 bg-line"></span>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ThunkStatusOrDeleted {event} {context} {status}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip tip-left" />
|
||||
hideZaps={hideZap}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
<EventActions {url} {event} {hideZap} {noun}>
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
{#if editForm && event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Button onclick={edit(editForm)}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
Edit {noun}
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
|
@ -18,9 +17,7 @@
|
|||
|
||||
const {url, event, hideZap, class: className = ""}: Props = $props()
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
|
||||
const onEmoji = (emoji: string) => publishReaction(event, {content: emoji, tags: []}, {url, h})
|
||||
const onEmoji = (emoji: string) => publishReaction(event, {content: emoji, tags: []}, {url})
|
||||
</script>
|
||||
|
||||
{#if ENABLE_ZAPS && !hideZap}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {ZapGoal} from "@welshman/domain"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {reader} from "@app/core"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
}
|
||||
|
||||
const {url, event, showRoom, showActivity, context}: Props = $props()
|
||||
|
||||
const goal = reader(ZapGoal)(event)
|
||||
|
||||
const h = goal.room()
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url})
|
||||
</script>
|
||||
|
||||
<div class="flex min-w-0 grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
hideZaps
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
import {reader} from "@app/core"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import type {GoalProgress} from "@app/goals"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
|
|
@ -57,6 +57,6 @@
|
|||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} {context} />
|
||||
<ContentActions showActivity hideZap noun="Goal" {url} {event} {context} />
|
||||
</div>
|
||||
</Cv>
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {publishReaction, retractReaction} from "@app/reactions"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
context: FeedContext
|
||||
}
|
||||
|
||||
const {url, event, showRoom, showActivity, context}: Props = $props()
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
{#if h && showRoom}
|
||||
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
|
||||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{context}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Poll" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
import Link from "@lib/components/Link.svelte"
|
||||
import Cv from "@lib/components/Cv.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import PollActions from "@app/components/PollActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
|
@ -33,6 +33,6 @@
|
|||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<PollActions showActivity {url} {event} {context} />
|
||||
<ContentActions showActivity noun="Poll" {url} {event} {context} />
|
||||
</div>
|
||||
</Cv>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@
|
|||
onEdit,
|
||||
}: Props = $props()
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profileDisplay = $profiles.display(event.pubkey, [url]).$
|
||||
const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks))
|
||||
|
|
@ -92,9 +91,9 @@
|
|||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url})
|
||||
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url, h})
|
||||
const createReaction = (values: EventContent) => publishReaction(event, values, {url})
|
||||
</script>
|
||||
|
||||
<TapTarget
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import {handles, profiles} from "@app/core"
|
||||
import {highlightedEvent, makeEventPermalink} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
{#if isComment}
|
||||
<CommentActions {event} {url} {context} />
|
||||
{:else}
|
||||
<ThreadActions {event} {url} {context} />
|
||||
<ContentActions noun="Thread" {event} {url} {context} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {removeUndefined} from "@welshman/lib"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import type {EventContent, TrustedEvent} from "@welshman/util"
|
||||
import {deletes, reactions, relays, wraps} from "@app/core"
|
||||
|
||||
export type ReactionTarget = {
|
||||
url?: string
|
||||
h?: string
|
||||
// Where to publish, when it isn't just the space's own relay.
|
||||
urls?: string[]
|
||||
}
|
||||
|
|
@ -13,12 +13,14 @@ export type ReactionTarget = {
|
|||
export const publishReaction = async (
|
||||
event: TrustedEvent,
|
||||
{content, tags}: EventContent,
|
||||
{url, h, urls}: ReactionTarget,
|
||||
{url, urls}: ReactionTarget,
|
||||
) => {
|
||||
const protect = url ? await relays.get().hasNip(url, 70) : false
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const command = await reactions.get().react(event, content, writer => {
|
||||
writer.addTags(...tags).setProtected(protect)
|
||||
|
||||
// welshman tags the room only once the tracker knows the target's relay, which an unconfirmed message lacks.
|
||||
if (url && h) {
|
||||
writer.setRoom(url, h)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import ContentMarkdown from "@app/components/ContentMarkdown.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import ArticleActions from "@app/components/ArticleActions.svelte"
|
||||
import ArticleActionBar from "@app/components/ArticleActionBar.svelte"
|
||||
import ArticleAuthorSidebar from "@app/components/ArticleAuthorSidebar.svelte"
|
||||
import EventComments from "@app/components/EventComments.svelte"
|
||||
import EventMenu from "@app/components/EventMenu.svelte"
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
</article>
|
||||
<div class="border-t px-5 py-4 sm:px-8" style="border-color: var(--line)">
|
||||
<div class="mx-auto flex w-full max-w-[68ch]">
|
||||
<ArticleActions event={$event} {url} {context} showActivity detail />
|
||||
<ArticleActionBar event={$event} {url} {context} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- lg carries this in the sidebar instead, which sits below the comments here. -->
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
|
||||
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
|
||||
|
|
@ -92,7 +93,13 @@
|
|||
<CalendarEventHeader event={$event} />
|
||||
<CalendarEventMeta event={$event} {url} />
|
||||
<CalendarRsvp {url} event={$event} rsvps={$rsvps} onShowPeople={showTab("people")} />
|
||||
<CalendarEventActions showRoom {url} event={$event} {context} />
|
||||
<ContentActions
|
||||
showRoom
|
||||
noun="Event"
|
||||
editForm={CalendarEventEdit}
|
||||
{url}
|
||||
event={$event}
|
||||
{context} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- One event carries three unrelated conversations, so give each its own tab instead of
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
<script lang="ts">
|
||||
import {onDestroy} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {sleep, uniq} from "@welshman/lib"
|
||||
import {Classified} from "@welshman/domain"
|
||||
import {normalizeTopic} from "@lib/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import ClassifiedEdit from "@app/components/ClassifiedEdit.svelte"
|
||||
import ClassifiedStatus from "@app/components/ClassifiedStatus.svelte"
|
||||
import EventComments from "@app/components/EventComments.svelte"
|
||||
import {events, reader} from "@app/core"
|
||||
import {makeFeedContext} from "@app/feeds"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {makeClassifiedPath} from "@app/routes"
|
||||
import type {PageProps} from "./$types"
|
||||
|
||||
const {params}: PageProps = $props()
|
||||
|
|
@ -23,11 +28,23 @@
|
|||
const event = $events.one(address, [url]).$
|
||||
const classified = derived(event, $event => ($event ? reader(Classified)($event) : undefined))
|
||||
|
||||
const topics = $derived(uniq(($classified?.topics() ?? []).map(normalizeTopic)))
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
onDestroy(context.cleanup)
|
||||
</script>
|
||||
|
||||
{#snippet topicLinks()}
|
||||
{#each topics as topic (topic)}
|
||||
<Link
|
||||
href="{makeClassifiedPath(url)}?topic={encodeURIComponent(topic)}"
|
||||
class="badge badge-neutral badge-sm font-normal">
|
||||
#{topic}
|
||||
</Link>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<SpaceBar {back}>
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{$classified?.title() ?? ""}</h1>
|
||||
|
|
@ -39,7 +56,18 @@
|
|||
<NoteCard event={$event} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={$event} {url} />
|
||||
<ClassifiedActions showRoom event={$event} {url} {context} />
|
||||
<ContentActions
|
||||
showRoom
|
||||
noun="Listing"
|
||||
editForm={ClassifiedEdit}
|
||||
labels={topics.length > 0 ? topicLinks : undefined}
|
||||
event={$event}
|
||||
{url}
|
||||
{context}>
|
||||
{#snippet status()}
|
||||
<ClassifiedStatus event={$event} />
|
||||
{/snippet}
|
||||
</ContentActions>
|
||||
</div>
|
||||
</NoteCard>
|
||||
<EventComments event={$event} {url} {context} />
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import ContentActions from "@app/components/ContentActions.svelte"
|
||||
import GoalMeter from "@app/components/GoalMeter.svelte"
|
||||
import GoalStatus from "@app/components/GoalStatus.svelte"
|
||||
import GoalSupporters from "@app/components/GoalSupporters.svelte"
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
{$progress.isFunded ? "Chip in anyway" : "Contribute to this goal"}
|
||||
</ZapButton>
|
||||
{/if}
|
||||
<GoalActions event={$event} {url} {context} />
|
||||
<ContentActions hideZap noun="Goal" event={$event} {url} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{#if $goal.summary()}
|
||||
|
|
|
|||
Loading…
Reference in a new issue