From 96c0073f3adc2ba15beec7026c167dbda5915e81 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 30 Sep 2026 13:51:08 -0700 Subject: [PATCH] Fix regressions found auditing the changes since 1.11.2 --- e2e/specs/admin.spec.ts | 2 + e2e/specs/community.spec.ts | 22 +- package.json | 20 +- pnpm-lock.yaml | Bin 402823 -> 402823 bytes server/preview.js | 48 ++- server/test/preview.test.mjs | 6 + src/app/components/CallControlBar.svelte | 2 +- src/app/components/CommentCompose.svelte | 9 +- src/app/components/EventReply.svelte | 2 +- src/app/components/NoteCreate.svelte | 18 +- src/app/components/PinReference.svelte | 10 +- src/app/components/PinSave.svelte | 294 +++++++----------- src/app/components/PollVotes.svelte | 9 +- src/app/components/RoomItemMenu.svelte | 2 +- src/app/components/RoomItemMenuMobile.svelte | 2 +- .../components/RoomPinnedMessagesAll.svelte | 2 +- src/app/components/SpaceInvite.svelte | 2 +- src/app/components/SpaceMemberMap.svelte | 11 +- src/app/components/hosting/RelayCreate.svelte | 26 +- src/app/pinboards.ts | 64 ++-- src/app/push/adapters/common.ts | 27 +- src/app/push/index.ts | 2 +- src/app/reminders.ts | 14 +- src/app/roomPins.ts | 8 +- src/app/signer.ts | 4 +- src/lib/components/Button.svelte | 1 + src/lib/components/MenuButton.svelte | 25 +- src/lib/components/ScrollToTop.svelte | 2 +- src/routes/[bech32]/+page.svelte | 13 +- src/routes/settings/alerts/+page.svelte | 6 +- src/routes/settings/content/+page.svelte | 9 +- src/routes/settings/privacy/+page.svelte | 2 +- src/routes/settings/profile/+page.svelte | 6 +- .../[relay]/library/[address]/+page.svelte | 6 +- .../spaces/[relay]/threads/[id]/+page.svelte | 2 +- 35 files changed, 368 insertions(+), 310 deletions(-) diff --git a/e2e/specs/admin.spec.ts b/e2e/specs/admin.spec.ts index 783d446e..44309ba9 100644 --- a/e2e/specs/admin.spec.ts +++ b/e2e/specs/admin.spec.ts @@ -291,6 +291,8 @@ test("US-094 invite people to a space", async ({seed, as}) => { await expect(page.getByRole("alert")).toContainText("Members have successfully been added!") + // Adding members opened over the invite, so saving goes back to it. + await invite.getByRole("button", {name: "Done"}).click() await page.locator(".secondary-nav").getByRole("link", {name: "Directory"}).click() await expect(memberCard(page, "Nadia Newcomer")).toBeVisible() diff --git a/e2e/specs/community.spec.ts b/e2e/specs/community.spec.ts index 8e040f38..2f6ba8dc 100644 --- a/e2e/specs/community.spec.ts +++ b/e2e/specs/community.spec.ts @@ -782,20 +782,18 @@ test("US-054 curate the library", async ({seed, as}) => { await expect(page.getByRole("heading", {name: "Reading List"})).toBeVisible() await expect(page.getByText("#reading")).toBeVisible() - // Several links go in at once, each as a card of its own. + // Each link goes in on its own and becomes a card of its own. const nevent = nip19.neventEncode({id: message.id, kind: MESSAGE, relays: [url]}) - await page.getByRole("button", {name: "Add a link"}).click() + for (const link of ["https://handbook.test/style-guide", `nostr:${nevent}`]) { + await page.getByRole("button", {name: "Add a link"}).click() - const links = dialog(page, "Add a Link") + const links = dialog(page, "Add a Link") - await links - .getByPlaceholder("URL or nevent...") - .fill(`https://handbook.test/style-guide\nnostr:${nevent}`) - // A title and a description are about one link, so a batch is offered neither. - await expect(links.getByPlaceholder("Optional title")).toHaveCount(0) - - await links.getByRole("button", {name: "Add 2 links"}).click() + await links.getByLabel("Link", {exact: true}).fill(link) + await links.getByRole("button", {name: "Add link"}).click() + await expect(links).toHaveCount(0) + } await expect(page.locator('a[href="https://handbook.test/style-guide"]')).toBeVisible() await expect(page.getByText("the deploy broke again")).toBeVisible() @@ -810,7 +808,7 @@ test("US-054 curate the library", async ({seed, as}) => { const linkEdit = dialog(page, "Edit Link") - await linkEdit.getByPlaceholder("Optional title").fill("House style guide") + await linkEdit.getByLabel("Title", {exact: true}).fill("House style guide") await linkEdit.getByRole("button", {name: "Save changes"}).click() await expect(page.getByRole("alert")).toContainText("Link updated!") @@ -835,7 +833,7 @@ test("US-054 curate the library", async ({seed, as}) => { const fromPoll = dialog(page, "Add a Link") - await fromPoll.getByPlaceholder("Optional title").fill("Standup poll") + await fromPoll.getByLabel("Title", {exact: true}).fill("Standup poll") await fromPoll.getByRole("button", {name: "Add link"}).click() await expect(page.getByRole("alert")).toContainText("Link added!") diff --git a/package.json b/package.json index eee21583..3c595a7c 100644 --- a/package.json +++ b/package.json @@ -98,16 +98,16 @@ "@types/throttle-debounce": "^5.0.2", "@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/sveltekit": "^1.1.0", - "@welshman/app": "^0.12.2", - "@welshman/content": "^0.12.2", - "@welshman/domain": "^0.12.2", - "@welshman/editor": "^0.12.2", - "@welshman/feeds": "^0.12.2", - "@welshman/lib": "^0.12.2", - "@welshman/net": "^0.12.2", - "@welshman/signer": "^0.12.2", - "@welshman/store": "^0.12.2", - "@welshman/util": "^0.12.2", + "@welshman/app": "^0.12.3", + "@welshman/content": "^0.12.3", + "@welshman/domain": "^0.12.3", + "@welshman/editor": "^0.12.3", + "@welshman/feeds": "^0.12.3", + "@welshman/lib": "^0.12.3", + "@welshman/net": "^0.12.3", + "@welshman/signer": "^0.12.3", + "@welshman/store": "^0.12.3", + "@welshman/util": "^0.12.3", "cheerio": "^1.2.0", "compressorjs-next": "^1.1.2", "dompurify": "^3.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f00f49ca7e7309109ed418278e92cc7b89c9e3b..052382715e200081950a0af12d1ca0991c940e9a 100644 GIT binary patch delta 3595 zcmchaTWs6r6~{rFbW+!L(#B5HWOi#WYtjm}D3T(jU7C`*P*>_|-7pO0RTOn4Mcs(4 z$<{0$FeF>2Vc#BxB1Yeb0evt~>}4<8LxG@q?aPV*TTr0e(g0hxmvxk6%U#pL&d}t6 z0{Mxb&iQ`l@;};s=gjsyXMWRv48j3_2VQ~j;YX*J?T6ppfKS^G{Mt*SrTI(Xw?Bp# z?AzOCpFiZ)@cuTia~k|5JzuW?WEU5sIFWO z=x;z9V9A1r2_`#mi<7%cSE0wJ==Sb@@=thaytdFbn}&Sh>(8Q_E(|Z$RtdM>mm05c?i<8hqy-yfCJt#Y~I| zseLQk2nVYI9_+g+l#*?!9VU+_-C|m5wzPV=*wum|x~t%p6t)z#*EI}Nue-NG&Aihz zR4P~w>D7KK?rgYyQRg0=Z~qW_Y?RSQ0W-z`uG~acMh+XkJT-qK(Ra%IB;(Dum7K%n^FfiM2bkH7+oa- zfr?h5x+)**=6hB-Qy|!8#23!xh;B00au5MEuEyiE81K`)a;E3ajQD>Q*g%%QFg5lQ zfty3lj2)?(Mom)-Qp;TuTN;z`8(~L-4VH>{LpI%MlIiCwO}d>#r9wU%uslxO-dU9kZTa6NA&O=oxvLDo{ z1(X^@vpxw8`ntSgHEA;wYs7W0izPEgmC0jzI1>%%{*YgfkQt$qHhjGVjW%h6H65&* zrD8=j5CZEzn>qif^Nt|T;`8(1`+tQ`0rU<$H!_`zHrqnlT_W7QT(wtqC&E2L@mNg3 zr*-r~BHfS@S(UHWtm>fTFLEVM4C73-nCSIQB2s5Nt+dn90xf5)pOm|uPOwxdr zQk#El!6(3lt2XxiKDJkkBVQ(&_N%MV`q+cNKfL&#vO9=!8G62lUMf2XfiyEJ;b%rI zeF;4I7dZcDaEx7UegH2Waikdc(<`~0J#UtM}P{Rcz1!1!GBz-LPlL7he1HB=Nak z0D1{Y+22e{hH R=jXOBpG0;qpPW0s@?VkTcsKw6 delta 3533 zcmds4OOM;u6$U-z*qP3Z?bzcucCFYGH+CXqD84DQO`^9bN}?!{k|-2KNJ`ZE{Up)G zb)9+&6b=%o=*=QXGRVFtki`H&7wx891PJ0^5U*kc=))PHNV{oD%JXt!)Z-%Yq6^~c zUh&>@&iCDOzQgqoPp^M?`gfDZARMT-;S~sd8ao|o|yi5Wp&>cU1wRG#x=^L8>GsV42q)g5?$0wjHjrURA&qL?N6Y?(;uEX zv$M~^VFt~&kCP4fW$@lV;3uZlC->P0S1ss?>HGVa?#GsRro7MRhat?Sr#x+Zcb~}v zguKhQ?e(+M+vn7M#%I?DY=Z>4ia=i5hbS1hXVpF z2GiYSglRM@EWwLrFgIrXv>)%wOxVz?94cV(AX-nwRX!ooRke&KJ(#86bk@gkc*n*$3hYQ!BbM{~(=M3*y(0P0Kg9SPN-LfE;J$z|F-=BHKHzLJa zNgI;22BT6#y*z4W3kA2?%PCP`ND!z&ODg+ttY?;dR=^+S83S{NnkH&yOKKI9a3;ye z1LI_`)d_SlkIzr`Yhj<`)p_vCcabF!|0#3|{CEpqbj-QJya+12Y8n2 zgU;K~)8I$HfF5%$@RA+geGza%2di7~isRF&l@Fz@O0G7_+BM}m zbP>FshK_H#UV+XY+5=tr8I+oVj*i+E%TT6?8AQ=t!xb`Ajzx^PsHuLro-PNy?nxIn zGC40zlv0u=u@=XVvKiSbO+vC8?TiEc9^DqQT%M&el}cSpE6fb^RR;#AmfcJKHtWfG zg8V!0(wbGxw-DaR*u z!DbII#@Pt#DGd3VAshV$mcXh?G394Ebh)7tgJzs7HZo~tBqc_@vcI}*`I8QO z6kK{0dU_A&z0391tI&m+2iXx_i^b$tPd5arVTObprH>PRG26$Ys#b~S25m-Fx+qiT z2G)=g%BHs?4f{SiUh9_W7@3cFaf9l0#e6Kx+i56a4UN1v_HCB8>9v!34ZLs^S(tH* z^LVbA;6!pXXiL$69%d7%K~)*Y++;2uB``nA#mtEq*T#c(Sz|cfGcE+PX{nJWla*jt z%Arb32ou7%DDw2At?6t$GrXH>FTP~Q|Lg0JZ#SKm*1xz8+0P;SncMv0It;@$Hs8K# zIC}e+T^8=I42i?%J!=@g_HaUO>%z<6%hzWOz+Oq;G~a-E>A?yPR2_cGDa*^?@lWC0 zqrqb4a{DuQ`G6h5cC|c!-hDHz(2m6KiPJYsSo1*r%01!@*6+ZHz3$CcZQ%bLzB22& zo&kTq0iOfN7F;?2qTu+gIoEy_`v1Z8>xrO&TmOU?ZB!oAR)UQWU>V%`;LyTm^Iz~; zrz+g7R(IQMr%xg`&;A>}Wb5Rk1?+os2=)jo2k_4$7j1jcpXSbiH#guV@Wwon+zt9G z-yQ^b_T{ diff --git a/server/preview.js b/server/preview.js index 35e71ad8..73cf3270 100644 --- a/server/preview.js +++ b/server/preview.js @@ -3,7 +3,13 @@ import {Hono} from "hono" import {load} from "cheerio" import {ParsedType, parse, renderAsText, summarize} from "@welshman/content" import {MINUTE, ellipsize, first, identity, int, ms, sleep, tryCatch} from "@welshman/lib" -import {displayRelayUrl, getIdFilters, normalizeRelayUrl} from "@welshman/util" +import { + displayRelayUrl, + getIdFilters, + isIPAddress, + isShareableRelayUrl, + normalizeRelayUrl, +} from "@welshman/util" import { Article, Classified, @@ -13,6 +19,7 @@ import { ZapGoal, displayPubkey, } from "@welshman/domain" +import {LOCAL_RELAY_URL, MockAdapter} from "@welshman/net" import {Domain, Network, Profiles, Relays, Rooms, createApp, makeRoomKey} from "@welshman/app" const PLATFORM_NAME = process.env.VITE_PLATFORM_NAME @@ -21,7 +28,29 @@ const fromCsv = value => (value || "").split(",").filter(identity) const defaultRelays = fromCsv(process.env.VITE_DEFAULT_RELAYS).map(normalizeRelayUrl) +// Anyone can put any host in a url or an event's tags, so the server only reaches public relays. +const isPublicRelayUrl = url => isShareableRelayUrl(url) && !isIPAddress(url) + +const toRelayUrl = value => { + const url = tryCatch(() => normalizeRelayUrl(value)) + + return url && isPublicRelayUrl(url) ? url : undefined +} + +// Every socket goes through here, so a relay that isn't public refuses rather than being dialed. +const refuseRelay = url => { + const adapter = new MockAdapter(url, ([verb, id]) => { + if (verb === "REQ") { + setTimeout(() => adapter.receive(["CLOSED", id, "blocked: not a public relay"])) + } + }) + + return adapter +} + const app = createApp({ + getAdapter: url => + url === LOCAL_RELAY_URL || isPublicRelayUrl(url) ? undefined : refuseRelay(url), config: { getDefaultRelays: () => defaultRelays, getIndexerRelays: () => fromCsv(process.env.VITE_INDEXER_RELAYS).map(normalizeRelayUrl), @@ -200,14 +229,17 @@ const remember = async (key, describe) => { return cached.metadata } - const metadata = await describe() + // A relay that stalls leaves the page to go out without a preview rather than hold it open. + const metadata = await Promise.race([describe(), sleep(ms(4))]) - if (cache.size > 500) { - cache.clear() + if (metadata) { + if (cache.size > 500) { + cache.clear() + } + + cache.set(key, {metadata, expiresAt: Date.now() + ms(int(5, MINUTE))}) } - cache.set(key, {metadata, expiresAt: Date.now() + ms(int(5, MINUTE))}) - return metadata } @@ -234,7 +266,7 @@ export const createPreview = template => { // A space relay answers nothing without auth, so most of these fall back to its nip-11 profile. const renderSpace = describe => render(async (url, params) => { - const relayUrl = tryCatch(() => normalizeRelayUrl(params.relay)) + const relayUrl = toRelayUrl(params.relay) if (relayUrl) { const space = await loadSpace(relayUrl) @@ -248,7 +280,7 @@ export const createPreview = template => { preview.get( "/join", render(async url => { - const relayUrl = tryCatch(() => normalizeRelayUrl(url.searchParams.get("r"))) + const relayUrl = toRelayUrl(url.searchParams.get("r")) if (relayUrl) { const space = await loadSpace(relayUrl) diff --git a/server/test/preview.test.mjs b/server/test/preview.test.mjs index 70a7f718..1444b94d 100644 --- a/server/test/preview.test.mjs +++ b/server/test/preview.test.mjs @@ -68,6 +68,12 @@ describe("preview metadata", () => { assert.equal(image, "https://app.flotilla.social/maskable-icon-512x512.png") }) + test("won't reach a relay on a private or local host", async () => { + for (const path of ["/join?r=localhost:8080", "/spaces/127.0.0.1", "/spaces/printer.local"]) { + assert.equal((await request(path)).title, "Flotilla") + } + }) + test("leaves the platform's own pages alone", async () => { for (const path of ["/", "/settings/profile", "/spaces/create", "/people/not-an-npub"]) { assert.equal((await request(path)).title, "Flotilla") diff --git a/src/app/components/CallControlBar.svelte b/src/app/components/CallControlBar.svelte index 78cde20f..6d973b68 100644 --- a/src/app/components/CallControlBar.svelte +++ b/src/app/components/CallControlBar.svelte @@ -175,7 +175,7 @@ diff --git a/src/app/components/CommentCompose.svelte b/src/app/components/CommentCompose.svelte index 752d0527..1998db19 100644 --- a/src/app/components/CommentCompose.svelte +++ b/src/app/components/CommentCompose.svelte @@ -1,7 +1,7 @@ @@ -176,88 +136,76 @@ Link {/snippet} {#snippet info()} -

- A URL or a nostr link (note, nevent, naddr, npub, or nprofile). Drop files here to upload - them. Paste several links, one per line, to file them all at once. -

+ {#if value.trim() && !pinReference} + Please enter a valid URL or nostr link. + {:else} + A URL or a nostr link, or upload a file. + {/if} {/snippet} {#snippet input()} -