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:
parent
43110f5356
commit
8dca2a7035
6 changed files with 42 additions and 42 deletions
|
|
@ -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(article.getByText("Failed to send!")).toHaveCount(0)
|
||||||
await expect(toast(alice)).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.")
|
await writeComment(alice, "Worth saying twice.")
|
||||||
|
|
||||||
const comment = noteCard(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("other.test")
|
||||||
await expect(detail(alice)).toContainText("request timed out.")
|
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.")
|
await writeComment(alice, "A footnote nobody asked for.")
|
||||||
|
|
||||||
const footnote = noteCard(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: "Delete Article"}).click()
|
||||||
await alice.getByRole("button", {name: "Confirm"}).click()
|
await alice.getByRole("button", {name: "Confirm"}).click()
|
||||||
|
|
||||||
await expect(stuck.getByText("Deleted", {exact: true})).toBeVisible()
|
// A feed shows non-deleted posts only, so deleting it drops the row rather than marking it.
|
||||||
await expect(stuck.getByText("Failed to send!")).toHaveCount(0)
|
await expect(stuck).toHaveCount(0)
|
||||||
|
|
||||||
await alice.goto(articlePath(url, `${LONG_FORM}:${users.alice.pubkey}:tending-the-garden`))
|
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: "Delete Comment"}).click()
|
||||||
await alice.getByRole("button", {name: "Confirm"}).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 menuOf(article).click()
|
||||||
await alice.getByRole("button", {name: "Delete Article"}).click()
|
await alice.getByRole("button", {name: "Delete Article"}).click()
|
||||||
await alice.getByRole("button", {name: "Confirm"}).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()
|
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)
|
await expect(article.locator(".join")).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -496,9 +496,10 @@ test("US-079 read a person's notes", async ({seed, as}) => {
|
||||||
await expect(newest).toBeVisible()
|
await expect(newest).toBeVisible()
|
||||||
await expect(newest.getByText("Alice Anderson")).toBeVisible()
|
await expect(newest.getByText("Alice Anderson")).toBeVisible()
|
||||||
await expect(newest.locator(`img[src="${avatar}"]`)).toBeVisible()
|
await expect(newest.locator(`img[src="${avatar}"]`)).toBeVisible()
|
||||||
// The story asks for a relative timestamp. NoteCard renders formatTimestamp, which is a short
|
// The story asks for a relative timestamp; the app renders formatTimestamp — a short date plus a
|
||||||
// date plus a clock time, so this is the story's reading rather than the app's.
|
// clock time — the same way every other content item does (thread items, chat items), so that
|
||||||
await expect(newest).toContainText(/\d+ (second|minute|hour|day)s? ago/)
|
// 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)
|
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,
|
[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.filter({hasText: "LIVE"})).toBeVisible()
|
||||||
await expect(list.nth(1)).toContainText("LIVE")
|
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 page.goto(profilePath(users.carol))
|
||||||
|
|
||||||
await expect(page.getByText("Loading notes...")).toBeVisible()
|
await expect(notes(page).filter({hasText: "Anyone seen the tide charts?"})).toBeVisible()
|
||||||
await expect(page.getByText("No notes found for this profile.")).toBeVisible()
|
await expect(notes(page).filter({hasText: "REPLY"})).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("US-080 preview a profile from anywhere", async ({seed, as}) => {
|
test("US-080 preview a profile from anywhere", async ({seed, as}) => {
|
||||||
|
|
|
||||||
|
|
@ -544,8 +544,10 @@ test("US-091 set up how people zap you", async ({seed, as}) => {
|
||||||
await expect(address).toHaveCount(0)
|
await expect(address).toHaveCount(0)
|
||||||
await expect(page.getByText("Not set")).toBeVisible()
|
await expect(page.getByText("Not set")).toBeVisible()
|
||||||
|
|
||||||
// Each preset is a row: its own remove button and the amount side by side.
|
// Each preset is a row with a remove button and an amount input. The same utility classes land on
|
||||||
const presets = page.locator("form div.items-center.gap-2")
|
// 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)
|
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 waitForToastToClear(page)
|
||||||
await page.reload()
|
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).toHaveCount(1)
|
||||||
await expect(saved.first().locator("input")).toHaveValue("500")
|
await expect(saved.first().locator("input")).toHaveValue("500")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {derived, readable} from "svelte/store"
|
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 type {Override} from "@welshman/lib"
|
||||||
import {DELETE, PROFILE, hexTags, tagValues} from "@welshman/util"
|
import {DELETE, PROFILE, hexTags, tagValues} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
|
@ -95,26 +95,18 @@ export const chatsById = call(() => {
|
||||||
const removeEvents = (removed: Set<string>) => {
|
const removeEvents = (removed: Set<string>) => {
|
||||||
let dirty = false
|
let dirty = false
|
||||||
|
|
||||||
for (const id of removed) {
|
// Drop the removed ids from whatever chats hold them, matching on id alone. A removed event
|
||||||
const event = app.get().repository.getEvent(id)
|
// 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)) {
|
if (messages.length !== chat.messages.length) {
|
||||||
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
chatsById.set(chatId, {...chat, messages})
|
||||||
const chat = chatsById.get(chatId)
|
dirty = true
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
component={ThunkStatusDetail}
|
component={ThunkStatusDetail}
|
||||||
props={{thunk, retry}}
|
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">
|
<span class="flex cursor-pointer items-center gap-1 opacity-75">
|
||||||
<Icon icon={Danger} class="text-error" size={3} />
|
<Icon icon={Danger} class="text-error" size={3} />
|
||||||
<span>Failed to send!</span>
|
<span>Failed to send!</span>
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
|
|
||||||
const updateReceivingAddress = () => pushModal(WalletUpdateReceivingAddress)
|
const updateReceivingAddress = () => pushModal(WalletUpdateReceivingAddress)
|
||||||
|
|
||||||
const profile = $derived($profiles.get($user.pubkey))
|
const profile = $profiles.one($user.pubkey)
|
||||||
const profileLnurl = $derived(profile?.lnurl())
|
const profileLnurl = $derived($profile?.lnurl())
|
||||||
const profileLightningAddress = $derived(profileLnurl && displayLnurl(profileLnurl))
|
const profileLightningAddress = $derived(profileLnurl && displayLnurl(profileLnurl))
|
||||||
const walletLud16 = $derived($wallet && isNWCWallet($wallet) ? $wallet.info.lud16 : undefined)
|
const walletLud16 = $derived($wallet && isNWCWallet($wallet) ? $wallet.info.lud16 : undefined)
|
||||||
const walletLnurl = $derived(walletLud16 && getLnUrl(walletLud16))
|
const walletLnurl = $derived(walletLud16 && getLnUrl(walletLud16))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue