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.

This commit is contained in:
Jon Staab 2026-08-27 21:24:46 -07:00
parent 43110f5356
commit 8dca2a7035
6 changed files with 42 additions and 42 deletions

View file

@ -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)
})

View file

@ -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}) => {

View file

@ -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")

View file

@ -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<string>) => {
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
}
}

View file

@ -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"}}>
<span class="flex cursor-pointer items-center gap-1 opacity-75">
<Icon icon={Danger} class="text-error" size={3} />
<span>Failed to send!</span>

View file

@ -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))