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} -
  • - -
  • - {/if} - {/snippet} -
    + + + {#if showActivity} + + {/if} + + {#snippet customActions()} + {#if event.pubkey === $user.pubkey} +
  • + +
  • + {/if} + {/snippet} +
    +
    diff --git a/src/app/components/Chat.svelte b/src/app/components/Chat.svelte index 94ae8fb6..ea5462f7 100644 --- a/src/app/components/Chat.svelte +++ b/src/app/components/Chat.svelte @@ -179,7 +179,7 @@ eventToEdit = event } - const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent) + const onEditPrevious = () => ifLet($chat?.messages.find(canEditEvent), onEditEvent) let loading = $state(true) let compose: ChatCompose | undefined = $state() @@ -300,7 +300,7 @@ {#each missingRelayLists as pubkey (pubkey)} {/each} - to enable direct messaging by opening this conversation in their $app. + to enable direct messaging by opening this conversation in their client.

    diff --git a/src/app/components/ClassifiedActions.svelte b/src/app/components/ClassifiedActions.svelte index b33a7ac4..e20cb30a 100644 --- a/src/app/components/ClassifiedActions.svelte +++ b/src/app/components/ClassifiedActions.svelte @@ -55,23 +55,25 @@ {/each} - - - - {#if showActivity} - - {/if} - - {#snippet customActions()} - {#if event.pubkey === $user.pubkey} -
  • - -
  • - {/if} + {#snippet status()} + {/snippet} -
    + + {#if showActivity} + + {/if} + + {#snippet customActions()} + {#if event.pubkey === $user.pubkey} +
  • + +
  • + {/if} + {/snippet} +
    +
    diff --git a/src/app/components/CommentActions.svelte b/src/app/components/CommentActions.svelte index b675888f..90fc1589 100644 --- a/src/app/components/CommentActions.svelte +++ b/src/app/components/CommentActions.svelte @@ -28,11 +28,12 @@
    - - - {#if showActivity && path} - - {/if} - + + + {#if showActivity && path} + + {/if} + +
    diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index 2db4660b..cd673585 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -13,6 +13,7 @@ isInvoice, isLink, isProfile, + isRoom, isEvent, isEllipsis, isAddress, @@ -35,7 +36,9 @@ import ContentQuote from "@app/components/ContentQuote.svelte" import ContentTopic from "@app/components/ContentTopic.svelte" import ContentMention from "@app/components/ContentMention.svelte" + import RoomName from "@app/components/RoomName.svelte" import {entityLink} from "@app/env" + import {makeRoomPath} from "@app/routes" import {userSettingsValues} from "@app/settings" type Props = { @@ -182,6 +185,10 @@ {/if} {:else if isProfile(parsed)} + {:else if isRoom(parsed)} + + # + {:else if isQuote(parsed)} {#if isBlock(i)} diff --git a/src/app/components/GoalActions.svelte b/src/app/components/GoalActions.svelte index 315ae032..67b2deec 100644 --- a/src/app/components/GoalActions.svelte +++ b/src/app/components/GoalActions.svelte @@ -36,10 +36,11 @@ Posted in # {/if} - - - {#if showActivity} - - {/if} - + + + {#if showActivity} + + {/if} + + diff --git a/src/app/components/PollActions.svelte b/src/app/components/PollActions.svelte index c2458912..6b258fb2 100644 --- a/src/app/components/PollActions.svelte +++ b/src/app/components/PollActions.svelte @@ -33,10 +33,11 @@ Posted in # {/if} - - - {#if showActivity} - - {/if} - + + + {#if showActivity} + + {/if} + + diff --git a/src/app/components/ProfileTrust.svelte b/src/app/components/ProfileTrust.svelte index 308ebcef..02b23e2a 100644 --- a/src/app/components/ProfileTrust.svelte +++ b/src/app/components/ProfileTrust.svelte @@ -63,7 +63,8 @@

    {$followers.length} - {$followers.length === 1 ? "person" : "people"} you follow also follow + {$followers.length === 1 ? "person" : "people"} you follow also + {$followers.length === 1 ? "follows" : "follow"} {$profileDisplay}.

    diff --git a/src/app/components/RelayAdd.svelte b/src/app/components/RelayAdd.svelte index 26a47714..d42b5733 100644 --- a/src/app/components/RelayAdd.svelte +++ b/src/app/components/RelayAdd.svelte @@ -18,7 +18,7 @@ import ModalFooter from "@lib/components/ModalFooter.svelte" import RelayItem from "@app/components/RelayItem.svelte" import {pushToast} from "@app/toast" - import {app} from "@app/core" + import {app, blockedRelayLists, user} from "@app/core" interface Props { relays: Readable @@ -56,6 +56,7 @@ const loading = $state(new SvelteSet()) const relaySearch = $app.use(Relays).relaySearch + const blockedRelayUrls = $blockedRelayLists.urls($user.pubkey).$ const searchResults = $derived( $relaySearch @@ -64,6 +65,7 @@ if (matchRelay?.(url) === false) return false if ($relays.includes(url)) return false if (isIPAddress(url)) return false + if ($blockedRelayUrls.includes(url)) return false return true }) diff --git a/src/app/components/ReportMenuList.svelte b/src/app/components/ReportMenuList.svelte index 994f00fe..9e316812 100644 --- a/src/app/components/ReportMenuList.svelte +++ b/src/app/components/ReportMenuList.svelte @@ -41,7 +41,7 @@ if (error) { pushToast({theme: "error", message: error}) } else { - pushToast({message: "Content has successfully been deleted!"}) + pushToast({message: "Report has successfully been dismissed!"}) $app.repository.removeEvent(event.id) onResolved?.() } diff --git a/src/app/components/RoomChat.svelte b/src/app/components/RoomChat.svelte index 70bada97..a7c1b869 100644 --- a/src/app/components/RoomChat.svelte +++ b/src/app/components/RoomChat.svelte @@ -10,7 +10,7 @@ import type {Maybe} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util" - import {MembershipStatus, publish} from "@welshman/app" + import {MembershipStatus} from "@welshman/app" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import Login2 from "@assets/icons/login-3.svg?dataurl" @@ -31,7 +31,7 @@ import VideoCallContent from "@app/components/VideoCallContent.svelte" import CallControlBar from "@app/components/CallControlBar.svelte" import {deletes, relays, rooms, thunks, user} from "@app/core" - import {publishRoomJoinRequest} from "@app/access" + import {joinRoom, leaveRoom} from "@app/access" import {CallState, callTargetRoom, callState, VideoCallLayout, videoCallLayout} from "@app/call" import { PROTECTED, @@ -130,18 +130,14 @@ joining = true try { - const thunk = await (inviteCode - ? publishRoomJoinRequest(url, h, inviteCode) - : $rooms.joinRoom(url, {h}).then(publish)) + const message = await joinRoom(url, h, inviteCode) - const message = await thunk.waitForError() - - if (message && !message.startsWith("duplicate:")) { - return pushToast({theme: "error", message}) + if (message) { + pushToast({theme: "error", message}) + } else { + // Restart the feed now that we're a member + start() } - - // Restart the feed now that we're a member - start() } finally { joining = false } @@ -151,12 +147,14 @@ const leave = async () => { if (h) { leaving = true - try { - const thunk = await $rooms.leaveRoom(url, {h}).then(publish) - const message = await thunk.waitForError() - if (message && !message.startsWith("duplicate:")) { + try { + const message = await leaveRoom(url, h) + + if (message) { pushToast({theme: "error", message}) + } else { + pushToast({message: "You have left the room."}) } } finally { leaving = false diff --git a/src/app/components/RoomCompose.svelte b/src/app/components/RoomCompose.svelte index cfaf8587..69ffcc78 100644 --- a/src/app/components/RoomCompose.svelte +++ b/src/app/components/RoomCompose.svelte @@ -28,7 +28,8 @@ const {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props() - const draftKey = url || h ? new DraftKey(`room:${url ?? ""}:${h ?? ""}`) : undefined + const draftKey = + (url || h) && !initialValues ? new DraftKey(`room:${url ?? ""}:${h ?? ""}`) : undefined const autofocus = !isMobile diff --git a/src/app/components/RoomDetailMenu.svelte b/src/app/components/RoomDetailMenu.svelte index b6224281..95146430 100644 --- a/src/app/components/RoomDetailMenu.svelte +++ b/src/app/components/RoomDetailMenu.svelte @@ -1,7 +1,7 @@ diff --git a/src/app/components/ThunkStatusOrDeleted.svelte b/src/app/components/ThunkStatusOrDeleted.svelte index cd4d8787..3efb266c 100644 --- a/src/app/components/ThunkStatusOrDeleted.svelte +++ b/src/app/components/ThunkStatusOrDeleted.svelte @@ -8,10 +8,11 @@ type Props = { event: TrustedEvent + status?: Snippet children?: Snippet } - const {event, children}: Props = $props() + const {event, status, children}: Props = $props() const deleted = deriveIsDeleted(event) const history = $thunks.history @@ -20,8 +21,11 @@ {#if $deleted}
    Deleted
    -{:else if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)} - -{:else if children} +{:else} + {#if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)} + + {:else} + {@render status?.()} + {/if} {@render children?.()} {/if} diff --git a/src/app/management.ts b/src/app/management.ts index 232685ec..aa35c387 100644 --- a/src/app/management.ts +++ b/src/app/management.ts @@ -43,6 +43,8 @@ export const deriveSpaceSupportedMethods = simpleCache(([url]: [string | undefin // User +// A relay answers supportedmethods with everything it implements rather than with what the +// caller may use, so all this can tell us is that the call wasn't refused outright. export const deriveUserIsSpaceAdmin = (url?: string) => derived(deriveSpaceSupportedMethods(url), $methods => $methods.length > 0) diff --git a/src/app/policies.ts b/src/app/policies.ts index 1f29e2c0..30963328 100644 --- a/src/app/policies.ts +++ b/src/app/policies.ts @@ -122,12 +122,12 @@ const trustPolicy = (socket: Socket) => { const mostlyRestrictedPolicy = (socket: Socket) => { let total = 0 - let restricted = 0 + let refused = 0 const pending = new Set() const updateStatus = (error?: string) => { - if (total > 5 && restricted > total / 2) { + if (total > 5 && refused > total / 2) { if (error) { return relaysMostlyRestricted.update(assoc(socket.url, error)) } @@ -136,6 +136,19 @@ const mostlyRestrictedPolicy = (socket: Socket) => { } } + // NIP-01 reserves "blocked: " for a ban and "restricted: " for lacking permission. + const countDetails = (details: string) => { + if (details.startsWith("auth-required: ")) { + total-- + updateStatus() + } + + if (details.startsWith("restricted: ") || details.startsWith("blocked: ")) { + refused++ + updateStatus(details) + } + } + const unsubscribers = [ on(socket, SocketEvent.Receive, (message: RelayMessage) => { if (isRelayOk(message)) { @@ -145,15 +158,7 @@ const mostlyRestrictedPolicy = (socket: Socket) => { pending.delete(id) if (!ok) { - if (details.startsWith("auth-required: ")) { - total-- - updateStatus() - } - - if (details.startsWith("restricted: ")) { - restricted++ - updateStatus(details) - } + countDetails(details) } } } @@ -163,16 +168,7 @@ const mostlyRestrictedPolicy = (socket: Socket) => { if (pending.has(id)) { pending.delete(id) - - if (details.startsWith("auth-required: ")) { - total-- - updateStatus() - } - - if (details.startsWith("restricted: ")) { - restricted++ - updateStatus(details) - } + countDetails(details) } } }), diff --git a/src/routes/settings/hosting/+page.svelte b/src/routes/settings/hosting/+page.svelte index 29d34450..c5113ac9 100644 --- a/src/routes/settings/hosting/+page.svelte +++ b/src/routes/settings/hosting/+page.svelte @@ -2,7 +2,7 @@ import {onMount} from "svelte" import {App} from "@capacitor/app" import type {Maybe} from "@welshman/lib" - import {indexBy, spec} from "@welshman/lib" + import {indexBy, sortBy, spec} from "@welshman/lib" import Add from "@assets/icons/add.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl" @@ -107,7 +107,7 @@ const [t, inv, rel, draft] = results if (t.status === "fulfilled") tenant = t.value - if (inv.status === "fulfilled") invoices = inv.value + if (inv.status === "fulfilled") invoices = sortBy(invoice => -invoice.created_at, inv.value) if (rel.status === "fulfilled") relays = rel.value if (draft.status === "fulfilled") draftInvoice = draft.value diff --git a/src/routes/settings/wallet/+page.svelte b/src/routes/settings/wallet/+page.svelte index 23bdac7d..e55df318 100644 --- a/src/routes/settings/wallet/+page.svelte +++ b/src/routes/settings/wallet/+page.svelte @@ -283,7 +283,6 @@ diff --git a/src/routes/spaces/+page.svelte b/src/routes/spaces/+page.svelte index 6d787bea..f4b25706 100644 --- a/src/routes/spaces/+page.svelte +++ b/src/routes/spaces/+page.svelte @@ -300,7 +300,7 @@

    You haven't joined any spaces yet.

    {/if} {#if otherSpaces.length > 0} - {filteredUserUrls.length > 0 ? "More Spaces" : "Browse Spaces"} + Browse Spaces {/if} {#each otherSpaces.slice(0, limit) as relay (relay.url)}