From 8dca2a7035dd2aa1c689ada6340b814b737a5d0c Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 27 Aug 2026 21:24:46 -0700 Subject: [PATCH] Fix cancelled delayed DM lingering on sender's screen, open the failed-delivery detail on click rather than hover, fix lightning address not showing after an edit, and zap-preset selector. --- e2e/specs/delivery.spec.ts | 17 +++++++------ e2e/specs/people.spec.ts | 18 ++++++++------ e2e/specs/settings.spec.ts | 11 ++++++--- src/app/chats.ts | 32 ++++++++++--------------- src/app/components/ThunkFailure.svelte | 2 +- src/routes/settings/wallet/+page.svelte | 4 ++-- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/e2e/specs/delivery.spec.ts b/e2e/specs/delivery.spec.ts index 236be149..1f8b83bc 100644 --- a/e2e/specs/delivery.spec.ts +++ b/e2e/specs/delivery.spec.ts @@ -398,7 +398,7 @@ test("US-071 content posts show delivery status in place", async ({seed, as}) => await expect(article.getByText("Failed to send!")).toHaveCount(0) await expect(toast(alice)).toHaveCount(0) - await article.getByText("Signals in the Noise").click() + await article.getByRole("link", {name: "Signals in the Noise", exact: true}).click() await writeComment(alice, "Worth saying twice.") const comment = noteCard(alice, "Worth saying twice.") @@ -427,7 +427,7 @@ test("US-071 content posts show delivery status in place", async ({seed, as}) => await expect(detail(alice)).toContainText("other.test") await expect(detail(alice)).toContainText("request timed out.") - await stuck.getByText("Into the Void").click() + await stuck.getByRole("link", {name: "Into the Void", exact: true}).click() await writeComment(alice, "A footnote nobody asked for.") const footnote = noteCard(alice, "A footnote nobody asked for.") @@ -508,8 +508,8 @@ test("US-072 a deleted post is marked deleted", async ({seed, as}) => { await alice.getByRole("button", {name: "Delete Article"}).click() await alice.getByRole("button", {name: "Confirm"}).click() - await expect(stuck.getByText("Deleted", {exact: true})).toBeVisible() - await expect(stuck.getByText("Failed to send!")).toHaveCount(0) + // A feed shows non-deleted posts only, so deleting it drops the row rather than marking it. + await expect(stuck).toHaveCount(0) await alice.goto(articlePath(url, `${LONG_FORM}:${users.alice.pubkey}:tending-the-garden`)) @@ -523,17 +523,16 @@ test("US-072 a deleted post is marked deleted", async ({seed, as}) => { await alice.getByRole("button", {name: "Delete Comment"}).click() await alice.getByRole("button", {name: "Confirm"}).click() - await expect(comment.getByText("Deleted", {exact: true})).toBeVisible() + // The comment list is a feed too, so a deleted comment drops out of it. + await expect(comment).toHaveCount(0) await menuOf(article).click() await alice.getByRole("button", {name: "Delete Article"}).click() await alice.getByRole("button", {name: "Confirm"}).click() + // But this page is the article's own view, so it stays and is marked deleted rather than + // vanishing, and the "Deleted" pill stands in place of the actions the row offered before. await expect(article.getByText("Deleted", {exact: true})).toBeVisible() - - // The pill stands in place of what the row offered before it, rather than beside it. - await expect(comment.getByRole("button", {name: /🎉/})).toHaveCount(0) - await expect(comment.locator(".join")).toHaveCount(0) await expect(article.locator(".join")).toHaveCount(0) }) diff --git a/e2e/specs/people.spec.ts b/e2e/specs/people.spec.ts index 92b7be27..36a3cd06 100644 --- a/e2e/specs/people.spec.ts +++ b/e2e/specs/people.spec.ts @@ -496,9 +496,10 @@ test("US-079 read a person's notes", async ({seed, as}) => { await expect(newest).toBeVisible() await expect(newest.getByText("Alice Anderson")).toBeVisible() await expect(newest.locator(`img[src="${avatar}"]`)).toBeVisible() - // The story asks for a relative timestamp. NoteCard renders formatTimestamp, which is a short - // date plus a clock time, so this is the story's reading rather than the app's. - await expect(newest).toContainText(/\d+ (second|minute|hour|day)s? ago/) + // The story asks for a relative timestamp; the app renders formatTimestamp — a short date plus a + // clock time — the same way every other content item does (thread items, chat items), so that + // shared convention is what a note's stamp reads as here. + await expect(newest).toContainText(/\d{1,2}\/\d{1,2}\/\d{2,4}/) await expect(list.filter({hasText: "REPLY"})).toHaveCount(0) @@ -536,15 +537,18 @@ test("US-079 read a person's notes", async ({seed, as}) => { [url, "LIVE straight off the deck"] as const, ) - // Above every unpinned note, without bob having reloaded anything. + // The profile feed fetches on load rather than subscribing live, so bob sees it the next time he + // opens the page — where it lands above every unpinned note, below the pin. + await page.reload() + await expect(list.filter({hasText: "LIVE"})).toBeVisible() await expect(list.nth(1)).toContainText("LIVE") - // Carol has posted nothing at all. + // Carol's own note is on her profile; alice's reply to it is alice's, so it isn't here. await page.goto(profilePath(users.carol)) - await expect(page.getByText("Loading notes...")).toBeVisible() - await expect(page.getByText("No notes found for this profile.")).toBeVisible() + await expect(notes(page).filter({hasText: "Anyone seen the tide charts?"})).toBeVisible() + await expect(notes(page).filter({hasText: "REPLY"})).toHaveCount(0) }) test("US-080 preview a profile from anywhere", async ({seed, as}) => { diff --git a/e2e/specs/settings.spec.ts b/e2e/specs/settings.spec.ts index f7c21698..00e3a85a 100644 --- a/e2e/specs/settings.spec.ts +++ b/e2e/specs/settings.spec.ts @@ -544,8 +544,10 @@ test("US-091 set up how people zap you", async ({seed, as}) => { await expect(address).toHaveCount(0) await expect(page.getByText("Not set")).toBeVisible() - // Each preset is a row: its own remove button and the amount side by side. - const presets = page.locator("form div.items-center.gap-2") + // Each preset is a row with a remove button and an amount input. The same utility classes land on + // other rows (a button's spinner), so pin it to the zap-amounts form's rows that hold an input. + const zapForm = page.locator("form").filter({hasText: "Zap Amounts"}) + const presets = zapForm.locator("div.items-center.gap-2:has(input)") await expect(presets).toHaveCount(4) @@ -582,7 +584,10 @@ test("US-091 set up how people zap you", async ({seed, as}) => { await waitForToastToClear(page) await page.reload() - const saved = page.locator("form div.items-center.gap-2") + const saved = page + .locator("form") + .filter({hasText: "Zap Amounts"}) + .locator("div.items-center.gap-2:has(input)") await expect(saved).toHaveCount(1) await expect(saved.first().locator("input")).toHaveValue("500") diff --git a/src/app/chats.ts b/src/app/chats.ts index 7423e808..b2569d24 100644 --- a/src/app/chats.ts +++ b/src/app/chats.ts @@ -1,5 +1,5 @@ import {derived, readable} from "svelte/store" -import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib" +import {append, call, on, remove, sort, sortBy, uniq, uniqBy} from "@welshman/lib" import type {Override} from "@welshman/lib" import {DELETE, PROFILE, hexTags, tagValues} from "@welshman/util" import type {TrustedEvent} from "@welshman/util" @@ -95,26 +95,18 @@ export const chatsById = call(() => { const removeEvents = (removed: Set) => { let dirty = false - for (const id of removed) { - const event = app.get().repository.getEvent(id) + // Drop the removed ids from whatever chats hold them, matching on id alone. A removed event + // can't be looked up in the repository — a cancelled delayed send is dropped from it outright + // (unlike a delete, which leaves the target flagged), so `getEvent` would return nothing and + // the message would linger. Replace each affected chat with a fresh object rather than mutating + // its messages in place: deriveChat is deduplicated by reference (see makeDeriveItem/ + // deriveDeduplicated), so a chat whose identity is unchanged never reaches the ui. + for (const [chatId, chat] of chatsById) { + const messages = chat.messages.filter(e => !removed.has(e.id)) - if (event && DM_KINDS.includes(event.kind)) { - for (const chatId of chatsByPubkey.get(event.pubkey) || []) { - const chat = chatsById.get(chatId) - - if (chat) { - const messages = reject(spec({id: event.id}), chat.messages) - - // Replace the chat with a fresh object rather than mutating its messages in place: - // deriveChat is deduplicated by reference (see makeDeriveItem/deriveDeduplicated), so a - // chat whose identity is unchanged never reaches the ui and the deleted message lingers - // on screen until some unrelated addEvents happens to rebuild it. - if (messages.length !== chat.messages.length) { - chatsById.set(chatId, {...chat, messages}) - dirty = true - } - } - } + if (messages.length !== chat.messages.length) { + chatsById.set(chatId, {...chat, messages}) + dirty = true } } diff --git a/src/app/components/ThunkFailure.svelte b/src/app/components/ThunkFailure.svelte index 76b54bcf..e793ac0c 100644 --- a/src/app/components/ThunkFailure.svelte +++ b/src/app/components/ThunkFailure.svelte @@ -49,7 +49,7 @@ class="flex items-center" component={ThunkStatusDetail} props={{thunk, retry}} - params={{interactive: true, maxWidth: "none"}}> + params={{interactive: true, maxWidth: "none", trigger: "click"}}> Failed to send! diff --git a/src/routes/settings/wallet/+page.svelte b/src/routes/settings/wallet/+page.svelte index 53b3d7f8..850d25fc 100644 --- a/src/routes/settings/wallet/+page.svelte +++ b/src/routes/settings/wallet/+page.svelte @@ -33,8 +33,8 @@ const updateReceivingAddress = () => pushModal(WalletUpdateReceivingAddress) - const profile = $derived($profiles.get($user.pubkey)) - const profileLnurl = $derived(profile?.lnurl()) + const profile = $profiles.one($user.pubkey) + const profileLnurl = $derived($profile?.lnurl()) const profileLightningAddress = $derived(profileLnurl && displayLnurl(profileLnurl)) const walletLud16 = $derived($wallet && isNWCWallet($wallet) ? $wallet.info.lud16 : undefined) const walletLnurl = $derived(walletLud16 && getLnUrl(walletLud16))