From 5ecb811b24c309ba8dea9ab54399b090a6c234d2 Mon Sep 17 00:00:00 2001
From: Jon Staab
Date: Fri, 21 Aug 2026 08:31:53 -0700
Subject: [PATCH] Auto-cleanup
---
e2e/SUSPECTED_BUGS.md | 105 ------------------
e2e/USER_STORIES.md | 8 +-
e2e/specs/rooms.spec.ts | 22 +---
src/app/access.ts | 68 ++++++++----
src/app/components/ArticleActions.svelte | 13 ++-
src/app/components/ArticleItem.svelte | 7 +-
.../components/CalendarEventActions.svelte | 35 +++---
src/app/components/Chat.svelte | 4 +-
src/app/components/ClassifiedActions.svelte | 36 +++---
src/app/components/CommentActions.svelte | 13 ++-
src/app/components/Content.svelte | 7 ++
src/app/components/GoalActions.svelte | 13 ++-
src/app/components/PollActions.svelte | 13 ++-
src/app/components/ProfileTrust.svelte | 3 +-
src/app/components/RelayAdd.svelte | 4 +-
src/app/components/ReportMenuList.svelte | 2 +-
src/app/components/RoomChat.svelte | 30 +++--
src/app/components/RoomCompose.svelte | 3 +-
src/app/components/RoomDetailMenu.svelte | 16 ++-
src/app/components/RoomForm.svelte | 6 +-
src/app/components/RoomItemMenu.svelte | 13 +++
src/app/components/RoomItemMenuMobile.svelte | 11 ++
src/app/components/SpaceEdit.svelte | 2 +-
src/app/components/SpaceMember.svelte | 3 +-
src/app/components/SpaceTrustRelay.svelte | 3 +-
src/app/components/ThreadActions.svelte | 13 ++-
src/app/components/ThunkStatusDetail.svelte | 8 +-
.../components/ThunkStatusOrDeleted.svelte | 12 +-
src/app/management.ts | 2 +
src/app/policies.ts | 38 +++----
src/routes/settings/hosting/+page.svelte | 4 +-
src/routes/settings/wallet/+page.svelte | 1 -
src/routes/spaces/+page.svelte | 2 +-
.../spaces/[relay]/calendar/+page.svelte | 2 +-
.../[relay]/calendar/[address]/+page.svelte | 3 +-
35 files changed, 230 insertions(+), 295 deletions(-)
delete mode 100644 e2e/SUSPECTED_BUGS.md
diff --git a/e2e/SUSPECTED_BUGS.md b/e2e/SUSPECTED_BUGS.md
deleted file mode 100644
index b2dcab78..00000000
--- a/e2e/SUSPECTED_BUGS.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Suspected app bugs
-
-Places where the app source contradicts a story in `USER_STORIES.md`. The spec for each asserts
-the story, so it fails until the bug is fixed.
-
-## Rooms & membership
-
-- **US-019 / US-020 — joined and created rooms never land under "Your Rooms".** `Rooms.joinRoom`
- publishes only kind-9021 and `RoomForm.submit` publishes create/edit/join — none of them write
- the user's kind-10009 room list, which is what `deriveUserRooms` reads. Only the Favorite toggle
- and the delete path touch the list.
-- **US-019 — leaving a room's member list is silent on success.** `RoomDetailMenu.handleLoading`
- only toasts when `waitForError()` returns a message.
-- **US-021 — private-room card says "Join Room" where the story (and the restricted-room card)
- say "Ask to Join"** (`RoomChat.svelte` isPrivate vs isRestricted branches).
-- **US-028 — no Share action exists on any message.** `shareEvent` is wired only into EventMenu
- ("Share to Chat"), BoardMenu and PinMenu; the cross-space Share picker is reachable only from
- native share intents or `/share`.
-
-## Spaces & access
-
-- **US-011 — the "Request Access" state is unreachable on a `public_join = false` relay.**
- welshman's `RelayJoinWriter.setClaim("")` always writes a `claim` tag, so zooid answers
- "failed to validate invite code" instead of "no claim tag"; `attemptRelayAccess` swallows that
- error when no claim was supplied, so `SpaceJoin` shows "Join Space" and toasts "Welcome to the
- space!" for a space that refused the join.
-- **US-009 — "Browse Spaces" section renders as "More Spaces"** whenever the user has joined
- spaces, so the story's two named sections never appear together.
-
-## DMs & composer
-
-- **US-035 — up-arrow in an empty DM composer edits the oldest editable message, not the newest.**
- `Chat.svelte` does `messages.toReversed().find(canEditEvent)` but `chat.messages` is already
- newest-first; RoomChat uses the same expression over an ascending list, so the DM copy inverted
- the ordering.
-- **US-059 — a cancelled edit clobbers the room draft.** RoomChat re-keys RoomCompose on
- `initialValues` while passing the same `draftKey` during editing, so the draft store is
- overwritten with the edited message's text. Chat.svelte avoids this by passing
- `draftKey={eventToEdit ? undefined : draftKey}`.
-- **US-056 — mentions render with no avatar** (composer nodeview is a bare text span;
- `ContentMention` renders `@name` with no image).
-- **US-056 — `~room` references render as plain text once sent.** `parseRoom` pre-empts
- `parseLink` and `Content.svelte` has no `isRoom` branch, so the intended `#RoomName` link path
- in `ContentLinkUrl` is unreachable.
-- **US-057 — an unsupported file type is rejected silently.** `Uploader.addFile` returns false
- before any upload on a mime mismatch, so `onUploadError` (the only toast in the path) never runs.
-
-## Delivery & deletion
-
-- **US-072 — the "Deleted" pill never replaces reactions/menus.** In Article/Comment/Thread/Poll/
- Goal/CalendarEvent Actions, `ThunkStatusOrDeleted` is a sibling of `ReactionSummary`/
- `EventActions` instead of wrapping them; `ClassifiedActions` is the only one wired as intended.
-- **US-072 — a deleted event vanishes from lists and threads instead of staying with the pill.**
- `Repository` emits the target in `removed` and `makeFeed`/`deriveEventsById` prune on it; only
- `deriveEvent` (`includeDeleted: true`) retains it, so the pill is only reachable on detail pages.
-- **US-071 — the publish-status popover's timeout reason reads "timed out." not "request timed
- out".** `publishOne` always sets `detail: "timed out"`, making `ThunkStatusDetail`'s fallback
- branch unreachable.
-
-## People & profiles
-
-- **US-079 — profile notes show an absolute date+clock** (`NoteCard` uses `formatTimestamp`)
- where the story asks for a relative timestamp (`formatTimestampRelative` exists and is used for
- "Last active").
-- **US-079 — a note published live never appears in another user's open profile view without a
- reload.** `syncSpace`'s CONTENT_KINDS contains no NOTE kind and `makeFeed`'s forward scroller
- stops on its first tick.
-- **US-008 — deleting your profile can delete the blanked kind-0 it just published.** The kind-5
- requests carry an `a` tag (`0:`) newer than the blanked profile; zooid has no NIP-62
- handling, and in a fresh session no kind-5 may be sent at all.
-
-## Settings
-
-- **US-091 — the zero/negative zap-amount error toast is unreachable.** The inputs carry
- `min="1"`, so native constraint validation blocks submit before `onZapAmountsSubmit` runs.
-- **US-084 — blocked relays still appear in relay suggestions.** `RelayAdd` filters on
- matchRelay/already-selected/isIPAddress only; `BlockedRelayLists` is consumed solely by
- `policies.ts` to refuse sockets.
-
-## Content rendering
-
-- **US-067 — a bare `lnbc1…` invoice renders as plain prose.** `parseInvoice` only matches behind
- a `lightning:` scheme and `parseLink` skips it too, so the copyable chip appears only for the
- prefixed form.
-- **US-046 — the calendar "Today" divider keys off `created_at` instead of the event's start tag**
- (`isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()`), so it lands on
- the wrong item for past-published events. (Not covered by the suite; the scroll effect uses the
- correct `getStart`.)
-
-## Hosting & admin
-
-- **US-102 — payment history is not sorted client-side.** The hosting settings page renders
- `listTenantInvoices` order verbatim, so "most recent first" depends on the backend.
-- **US-041 — the article list card lacks the "Posted in #room" badge** the story names for both
- surfaces; `ArticleItem` never passes `showRoom` and renders the room inline in the byline
- instead.
-- **US-092 — `deriveUserIsSpaceAdmin` gates on `supportedMethods().length > 0`.** zooid's migration writes `member_methods = ["listclaims", "createclaim"]` — if applied, every member reports as an admin.
-
-## Known coverage gaps (not bugs)
-
-- **US-070 bullet 3** (article/comment retry succeeds and clears the indicator) cannot be
- exercised: no UI path makes an article publish fail then succeed in one session without
- discarding `$thunks.history`.
-- **US-065's "briefly shows a loading state"** depends on timing. Sync may already hold the
- quoted event when the assertion runs.
diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md
index 7c84e824..bfb8f6f5 100644
--- a/e2e/USER_STORIES.md
+++ b/e2e/USER_STORIES.md
@@ -406,14 +406,12 @@ Acceptance:
### US-028 — Share a message somewhere else
-As alice, I want to forward a message into another room or conversation, so that
-I can pass it along without retyping it.
+As alice, I want to forward a message into another room in the same space, so
+that I can pass it along without retyping it.
Acceptance:
-- "Share" on a message inside a space opens a picker of that space's rooms; from
- a context with no fixed space, it offers a search across all her rooms and
- conversations.
+- "Share" on a message inside a space opens a picker of that space's rooms.
- Choosing a destination navigates there with the composer pre-filled with a
quote of the shared message.
- Sending posts the quoted message in the destination, visible to bob there.
diff --git a/e2e/specs/rooms.spec.ts b/e2e/specs/rooms.spec.ts
index 23233cc1..73bfa632 100644
--- a/e2e/specs/rooms.spec.ts
+++ b/e2e/specs/rooms.spec.ts
@@ -1,8 +1,8 @@
-import {DAY, HOUR, MINUTE, WEEK} from "@welshman/lib"
+import {DAY, HOUR, WEEK} from "@welshman/lib"
import {MessagingRelayList, RelayList, displayPubkey} from "@welshman/domain"
import type {Locator, Page} from "@playwright/test"
import {expect, makeTestUser, roomPath, spacePath, test, users} from "../harness"
-import type {SeededRumor, SeededSpace, TestUser} from "../harness"
+import type {SeededSpace, TestUser} from "../harness"
// A handle to a seeded event. SeededEvent isn't exported from the harness, and only its id and
// timestamp are ever read back here.
@@ -709,8 +709,6 @@ test("US-027 find a past message and jump to it", async ({seed, as}) => {
})
test("US-028 share a message somewhere else", async ({seed, as}) => {
- let dm!: SeededRumor
-
const scenario = await seed(({relay, user, at}) => {
const space = relay("space")
@@ -724,8 +722,6 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
seedChatter(space, user.alice)
seedChatter(space, user.bob)
-
- dm = space.dm(user.bob, [user.alice], "are you around?", at(20, MINUTE))
})
const {url} = scenario.space("space")
@@ -754,18 +750,4 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
await expect(message(alice, "heads up")).toContainText("the dock is closed on sunday")
await expect(message(bob, "heads up")).toContainText("the dock is closed on sunday")
-
- // A conversation has no space of its own, so sharing from one searches everywhere she can post
- await alice.goto(`/chat/${users.bob.pubkey}`)
-
- const received = alice.locator(`[data-event="${dm.id}"]`)
-
- await expect(received).toContainText("are you around?")
-
- await received.hover()
- await received.getByRole("button").first().click()
- await alice.getByRole("button", {name: "Share"}).click()
-
- await expect(alice.getByText("Where would you like to share this?")).toBeVisible()
- await expect(alice.getByPlaceholder("Search rooms and conversations...")).toBeVisible()
})
diff --git a/src/app/access.ts b/src/app/access.ts
index 0b0b371f..f6b93a23 100644
--- a/src/app/access.ts
+++ b/src/app/access.ts
@@ -13,7 +13,7 @@ import {
RELAYS,
type ManagementResponse,
} from "@welshman/util"
-import {RelayJoin, RelayLeave, RoomJoin} from "@welshman/domain"
+import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
import {Sync, User, publish} from "@welshman/app"
import {stripPrefix} from "@lib/util"
import {app, command, relayManagement, roomLists, thunks, writer} from "@app/core"
@@ -100,20 +100,52 @@ export const deriveRelayAuthError = (url: string) =>
}
})
-export const publishJoinRequest = (url: string, claim = "") =>
- command(writer(RelayJoin).forceRelays(url).setClaim(claim)).then(publish)
+export const publishJoinRequest = (url: string, claim?: string) => {
+ const eventWriter = writer(RelayJoin).forceRelays(url)
+
+ if (claim) {
+ eventWriter.setClaim(claim)
+ }
+
+ return command(eventWriter).then(publish)
+}
export const publishLeaveRequest = (url: string) =>
command(writer(RelayLeave).forceRelays(url)).then(publish)
-export const publishRoomJoinRequest = (url: string, h: string, code?: string) => {
+// A relay answers a re-sent request with "duplicate:" and a membership it already has with
+// "already a member" — both leave us where we wanted to be, so only anything else is a refusal.
+const isMembershipRefusal = (error: string) =>
+ Boolean(error) && !error.startsWith("duplicate:") && !error.includes("already")
+
+// Joining a room takes two publishes: a NIP-29 request the relay can refuse, and the user's
+// own room list, which is what puts the room in their sidebar. `code` is a room invite code.
+export const joinRoom = async (url: string, h: string, code?: string) => {
const eventWriter = writer(RoomJoin).setRoom(url, h)
if (code) {
eventWriter.setClaim(code)
}
- return command(eventWriter).then(publish)
+ const thunk = await command(eventWriter).then(publish)
+ const error = await thunk.waitForError()
+
+ if (isMembershipRefusal(error)) {
+ return error
+ }
+
+ await roomLists.get().addRoom(h, url).then(publish)
+}
+
+export const leaveRoom = async (url: string, h: string) => {
+ const thunk = await command(writer(RoomLeave).setRoom(url, h)).then(publish)
+ const error = await thunk.waitForError()
+
+ if (isMembershipRefusal(error)) {
+ return error
+ }
+
+ await roomLists.get().removeRoom(h, url).then(publish)
}
export const publishRoomInvite = async (url: string, h: string) => {
@@ -198,12 +230,13 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
if (shouldIgnoreError(error)) return
- if (claim) {
- if (error.includes("invite code")) {
- return "join request rejected"
- }
- } else if (error.includes("invite code")) {
- return
+ if (error.includes("invite code")) {
+ return "join request rejected"
+ }
+
+ // A space that isn't open to the public refuses a join carrying no claim at all
+ if (error.includes("claim")) {
+ return "This space requires an invite code"
}
return stripPrefix(error)
@@ -316,17 +349,6 @@ export class Access {
}
}
- async joinRoom(h: string, code?: string) {
- if (!code) return
-
- const thunk = await publishRoomJoinRequest(this.url, h, code)
- const message = await thunk.waitForError()
-
- if (message && !message.startsWith("duplicate:")) {
- return message
- }
- }
-
async acceptInvite(data: InviteData, notifications: boolean) {
const spaceUrls = roomLists.get().urls(User.require(app.get()).pubkey).get()
const error = await this.joinSpace({
@@ -340,7 +362,7 @@ export class Access {
}
if (data.h && data.code) {
- return this.joinRoom(data.h, data.code)
+ return joinRoom(this.url, data.h, data.code)
}
}
diff --git a/src/app/components/ArticleActions.svelte b/src/app/components/ArticleActions.svelte
index 05bd459e..15ed4bba 100644
--- a/src/app/components/ArticleActions.svelte
+++ b/src/app/components/ArticleActions.svelte
@@ -46,10 +46,11 @@
{/each}
-
-
- {#if showActivity}
-
- {/if}
-
+
+
+ {#if showActivity}
+
+ {/if}
+
+
diff --git a/src/app/components/ArticleItem.svelte b/src/app/components/ArticleItem.svelte
index 53f4f9ad..af506e7d 100644
--- a/src/app/components/ArticleItem.svelte
+++ b/src/app/components/ArticleItem.svelte
@@ -7,7 +7,6 @@
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ArticleActions from "@app/components/ArticleActions.svelte"
- import RoomLink from "@app/components/RoomLink.svelte"
import {reader} from "@app/core"
import {makeArticlePath} from "@app/routes"
@@ -23,7 +22,6 @@
const title = article.title()
const summary = article.summary()
const image = article.image()
- const h = article.room()
Written by
- {#if h}
- in
- {/if}
-
+
diff --git a/src/app/components/CalendarEventActions.svelte b/src/app/components/CalendarEventActions.svelte
index 5d429b83..5199fbc5 100644
--- a/src/app/components/CalendarEventActions.svelte
+++ b/src/app/components/CalendarEventActions.svelte
@@ -41,21 +41,22 @@
Posted in #
{/if}
-
-
- {#if showActivity}
-
- {/if}
-
- {#snippet customActions()}
- {#if event.pubkey === $user.pubkey}
-