From 9a49aa66385a4db9b6829d3d733a745ccac9f4c9 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 25 Sep 2026 16:24:51 -0700 Subject: [PATCH] Replace the per-kind action bars with one ContentActions and give the article page its own ArticleActionBar --- .agents/skills/flotilla-architecture/SKILL.md | 2 +- .agents/skills/flotilla-model/kinds.md | 2 +- .agents/skills/flotilla-views/SKILL.md | 11 +- .agents/skills/flotilla-views/naming.md | 2 +- e2e/specs/articles-threads.spec.ts | 2 +- e2e/specs/delivery.spec.ts | 2 +- ...Actions.svelte => ArticleActionBar.svelte} | 40 +++---- src/app/components/ArticleActions.svelte | 92 -------------- src/app/components/ArticleItem.svelte | 20 +++- src/app/components/CalendarEventItem.svelte | 11 +- src/app/components/ClassifiedActions.svelte | 113 ------------------ src/app/components/ClassifiedItem.svelte | 5 +- src/app/components/CommentActions.svelte | 7 +- ...ntActions.svelte => ContentActions.svelte} | 63 +++++++--- src/app/components/EventReactButtons.svelte | 5 +- src/app/components/GoalActions.svelte | 54 --------- src/app/components/GoalItem.svelte | 4 +- src/app/components/PollActions.svelte | 50 -------- src/app/components/PollItem.svelte | 4 +- src/app/components/RoomItem.svelte | 5 +- src/app/components/ThreadPost.svelte | 4 +- src/app/reactions.ts | 6 +- .../[relay]/articles/[address]/+page.svelte | 4 +- .../[relay]/calendar/[address]/+page.svelte | 11 +- .../classifieds/[address]/+page.svelte | 34 +++++- .../spaces/[relay]/goals/[id]/+page.svelte | 4 +- 26 files changed, 159 insertions(+), 398 deletions(-) rename src/app/components/{ThreadActions.svelte => ArticleActionBar.svelte} (51%) delete mode 100644 src/app/components/ArticleActions.svelte delete mode 100644 src/app/components/ClassifiedActions.svelte rename src/app/components/{CalendarEventActions.svelte => ContentActions.svelte} (57%) delete mode 100644 src/app/components/GoalActions.svelte delete mode 100644 src/app/components/PollActions.svelte diff --git a/.agents/skills/flotilla-architecture/SKILL.md b/.agents/skills/flotilla-architecture/SKILL.md index 7508882e..c033fc67 100644 --- a/.agents/skills/flotilla-architecture/SKILL.md +++ b/.agents/skills/flotilla-architecture/SKILL.md @@ -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 diff --git a/.agents/skills/flotilla-model/kinds.md b/.agents/skills/flotilla-model/kinds.md index ffa97031..f7ee13b4 100644 --- a/.agents/skills/flotilla-model/kinds.md +++ b/.agents/skills/flotilla-model/kinds.md @@ -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` | diff --git a/.agents/skills/flotilla-views/SKILL.md b/.agents/skills/flotilla-views/SKILL.md index 3907ec4a..b597e3a0 100644 --- a/.agents/skills/flotilla-views/SKILL.md +++ b/.agents/skills/flotilla-views/SKILL.md @@ -238,8 +238,7 @@ New components go in the flat folder. ## Naming Names are ``: 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 ``. `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 `` 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 diff --git a/.agents/skills/flotilla-views/naming.md b/.agents/skills/flotilla-views/naming.md index 22eb186c..743661bf 100644 --- a/.agents/skills/flotilla-views/naming.md +++ b/.agents/skills/flotilla-views/naming.md @@ -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` | diff --git a/e2e/specs/articles-threads.spec.ts b/e2e/specs/articles-threads.spec.ts index 66dc46c3..01836910 100644 --- a/e2e/specs/articles-threads.spec.ts +++ b/e2e/specs/articles-threads.spec.ts @@ -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() diff --git a/e2e/specs/delivery.spec.ts b/e2e/specs/delivery.spec.ts index cd1cd2e1..95ed4d61 100644 --- a/e2e/specs/delivery.spec.ts +++ b/e2e/specs/delivery.spec.ts @@ -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) => diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ArticleActionBar.svelte similarity index 51% rename from src/app/components/ThreadActions.svelte rename to src/app/components/ArticleActionBar.svelte index 9ce8fefa..dcddb2ae 100644 --- a/src/app/components/ThreadActions.svelte +++ b/src/app/components/ArticleActionBar.svelte @@ -1,38 +1,29 @@ -
- {#if h && showRoom} - - Posted in # - - {/if} +
- {#if showActivity} - - {/if} - + reactionClass="tip-top" /> + {#if !$deleted} + + {#snippet leading()} + + {/snippet} + + {/if}
diff --git a/src/app/components/ArticleActions.svelte b/src/app/components/ArticleActions.svelte deleted file mode 100644 index 85c0f311..00000000 --- a/src/app/components/ArticleActions.svelte +++ /dev/null @@ -1,92 +0,0 @@ - - -{#if detail} -
- - - - {#if !$deleted} - - {#snippet leading()} - {#if showActivity} - - {/if} - {/snippet} - - {/if} -
-{:else} -
-
- {#if h && showRoom} - - Posted in # - - {/if} -
- {#each uniq(topics) as topic (topic)} - - {/each} -
- - - {#if showActivity} - - {/if} - -
- {#if !$deleted} - - {/if} -
-{/if} diff --git a/src/app/components/ArticleItem.svelte b/src/app/components/ArticleItem.svelte index f13d7273..65db8de8 100644 --- a/src/app/components/ArticleItem.svelte +++ b/src/app/components/ArticleItem.svelte @@ -1,5 +1,5 @@ +{#snippet topicLabels()} + {#each topics as topic (topic)} + #{topic} + {/each} +{/snippet} + @@ -55,7 +62,14 @@
- + 0 ? topicLabels : undefined} + {url} + {event} + {context} />
diff --git a/src/app/components/CalendarEventItem.svelte b/src/app/components/CalendarEventItem.svelte index c12f15f1..ece06e8b 100644 --- a/src/app/components/CalendarEventItem.svelte +++ b/src/app/components/CalendarEventItem.svelte @@ -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 {/if} - + diff --git a/src/app/components/ClassifiedActions.svelte b/src/app/components/ClassifiedActions.svelte deleted file mode 100644 index aed0590d..00000000 --- a/src/app/components/ClassifiedActions.svelte +++ /dev/null @@ -1,113 +0,0 @@ - - -
- {#if (h && showRoom) || (showTopics && uniq(topics).length > 0)} -
- {#if h && showRoom} - - Posted in # - - {/if} - {#if showTopics} - {#each uniq(topics) as topic (topic)} - - {/each} - {/if} -
- - {/if} - - {#snippet status()} - {#if showStatus} - - {/if} - {/snippet} - - {#if showActivity} - - {/if} - - {#snippet customActions()} - {#if event.pubkey === $user.pubkey} -
  • - -
  • - {/if} - {/snippet} -
    -
    -
    diff --git a/src/app/components/ClassifiedItem.svelte b/src/app/components/ClassifiedItem.svelte index 504e25a3..fc5e0e45 100644 --- a/src/app/components/ClassifiedItem.svelte +++ b/src/app/components/ClassifiedItem.svelte @@ -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 @@ {formatTimestampRelative(event.created_at)} - + diff --git a/src/app/components/CommentActions.svelte b/src/app/components/CommentActions.svelte index 505d9ee8..f34b6a66 100644 --- a/src/app/components/CommentActions.svelte +++ b/src/app/components/CommentActions.svelte @@ -1,6 +1,5 @@
    diff --git a/src/app/components/CalendarEventActions.svelte b/src/app/components/ContentActions.svelte similarity index 57% rename from src/app/components/CalendarEventActions.svelte rename to src/app/components/ContentActions.svelte index 1323e2ea..1c227cba 100644 --- a/src/app/components/CalendarEventActions.svelte +++ b/src/app/components/ContentActions.svelte @@ -1,4 +1,5 @@ -
    - {#if h && showRoom} - - Posted in # - +
    + {#if (h && showRoom) || labels} +
    + {#if h && showRoom} + + Posted in # + + {/if} + {@render labels?.()} +
    + {/if} - + + hideZaps={hideZap} + reactionClass="tip-left" /> {#if showActivity} {/if} - + {#snippet customActions()} - {#if event.pubkey === $user.pubkey} + {#if editForm && event.pubkey === $user.pubkey}
  • -
  • {/if} diff --git a/src/app/components/EventReactButtons.svelte b/src/app/components/EventReactButtons.svelte index 50cff15c..32f0e760 100644 --- a/src/app/components/EventReactButtons.svelte +++ b/src/app/components/EventReactButtons.svelte @@ -1,6 +1,5 @@ {#if ENABLE_ZAPS && !hideZap} diff --git a/src/app/components/GoalActions.svelte b/src/app/components/GoalActions.svelte deleted file mode 100644 index c1244659..00000000 --- a/src/app/components/GoalActions.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - {#if h && showRoom} - - Posted in # - - {/if} - - - {#if showActivity} - - {/if} - - -
    diff --git a/src/app/components/GoalItem.svelte b/src/app/components/GoalItem.svelte index afaf2fa5..22ec59ec 100644 --- a/src/app/components/GoalItem.svelte +++ b/src/app/components/GoalItem.svelte @@ -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 {/if} - +
    diff --git a/src/app/components/PollActions.svelte b/src/app/components/PollActions.svelte deleted file mode 100644 index adc335c2..00000000 --- a/src/app/components/PollActions.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - -
    - {#if h && showRoom} - - Posted in # - - {/if} - - - {#if showActivity} - - {/if} - - -
    diff --git a/src/app/components/PollItem.svelte b/src/app/components/PollItem.svelte index 1a41d2a0..a76fab5a 100644 --- a/src/app/components/PollItem.svelte +++ b/src/app/components/PollItem.svelte @@ -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 {/if} - +
    diff --git a/src/app/components/RoomItem.svelte b/src/app/components/RoomItem.svelte index 764aeec3..79b1dddd 100644 --- a/src/app/components/RoomItem.svelte +++ b/src/app/components/RoomItem.svelte @@ -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}) {:else} - + {/if}
    diff --git a/src/app/reactions.ts b/src/app/reactions.ts index 775e3bb6..be53b971 100644 --- a/src/app/reactions.ts +++ b/src/app/reactions.ts @@ -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) } diff --git a/src/routes/spaces/[relay]/articles/[address]/+page.svelte b/src/routes/spaces/[relay]/articles/[address]/+page.svelte index abd42136..b46be76d 100644 --- a/src/routes/spaces/[relay]/articles/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/articles/[address]/+page.svelte @@ -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 @@
    - +
    diff --git a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte index 1a8b7cb1..0b65d11a 100644 --- a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte @@ -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 @@ - +