Release the permalink pin when the reader jumps to the unread divider
This commit is contained in:
parent
0e694dc2ea
commit
3b8c217a25
6 changed files with 88 additions and 4 deletions
|
|
@ -442,6 +442,16 @@ Acceptance:
|
|||
message on screen and no "jump to newest" control, even when other events were
|
||||
published after the one linked to.
|
||||
|
||||
### US-027b — Jump to the unread divider from a permalink
|
||||
|
||||
As someone opening a push notification, I want the "New Messages" control to
|
||||
take me to where my unread messages start, so that I can read what I missed.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Landing on a permalink and then asking for new messages scrolls to the unread
|
||||
divider, and does not return to the message the permalink landed on.
|
||||
|
||||
### US-028 — Share a message somewhere else
|
||||
|
||||
As alice, I want to forward a message into another room in the same space, so
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type {BrowserContext} from "@playwright/test"
|
|||
import {ms} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {TestUser} from "../keys"
|
||||
import {injectEvents, injectSession} from "./session"
|
||||
import {injectChecked, injectEvents, injectSession} from "./session"
|
||||
|
||||
// Must match TEST_ENV_KEY in src/lib/test/env.ts.
|
||||
const TEST_ENV_KEY = "__TEST_ENV__"
|
||||
|
|
@ -19,6 +19,8 @@ export type BootOptions = {
|
|||
user?: TestUser
|
||||
// What this user's client already has in local storage, e.g. their room list.
|
||||
events?: TrustedEvent[]
|
||||
// Which paths this user has already read, keyed the way src/app/notifications.ts keys them.
|
||||
checked?: Record<string, number>
|
||||
path?: string
|
||||
// VITE_ values the scenario sets for itself. Anything named here has to be something the test owns.
|
||||
env?: Record<string, string>
|
||||
|
|
@ -26,7 +28,16 @@ export type BootOptions = {
|
|||
|
||||
export const boot = async (
|
||||
context: BrowserContext,
|
||||
{relays, indexers = relays, spaces = [], user, events = [], path = "/", env = {}}: BootOptions,
|
||||
{
|
||||
relays,
|
||||
indexers = relays,
|
||||
spaces = [],
|
||||
user,
|
||||
events = [],
|
||||
checked,
|
||||
path = "/",
|
||||
env = {},
|
||||
}: BootOptions,
|
||||
) => {
|
||||
const urls = relays.join(",")
|
||||
|
||||
|
|
@ -66,6 +77,10 @@ export const boot = async (
|
|||
await injectEvents(context, events)
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
await injectChecked(context, checked)
|
||||
}
|
||||
|
||||
const page = await context.newPage()
|
||||
|
||||
await page.goto(path)
|
||||
|
|
|
|||
|
|
@ -24,3 +24,15 @@ export const injectEvents = (context: BrowserContext, events: TrustedEvent[]) =>
|
|||
},
|
||||
[TEST_EVENTS_KEY, events] as const,
|
||||
)
|
||||
|
||||
// Read state as an earlier visit left it, so a room can open with unread messages already in it.
|
||||
export const injectChecked = (context: BrowserContext, checked: Record<string, number>) =>
|
||||
context.addInitScript(
|
||||
([key, value]) => {
|
||||
// An init script also runs on about:blank, where reading localStorage is a SecurityError.
|
||||
if (location.protocol.startsWith("http")) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
},
|
||||
["CapacitorStorage.checked", JSON.stringify(checked)] as const,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ export type PageOptions = {
|
|||
relayInfo?: RelayInfoOverrides
|
||||
// What the hosting backend already knows about this user; getHosting changes it mid-test.
|
||||
hosting?: HostingFixtures
|
||||
// Which paths this user has already read, for a spec that needs unread messages at first paint.
|
||||
checked?: Record<string, number>
|
||||
// Relay urls that take the socket and answer nothing; silenceRelay does the same mid-test.
|
||||
silent?: string[]
|
||||
// Relay urls that serve events and never eose, so every request runs out its own deadline.
|
||||
|
|
@ -276,6 +278,7 @@ export const test = base.extend<HarnessFixtures, HarnessWorkerFixtures>({
|
|||
user,
|
||||
path,
|
||||
env: options.env,
|
||||
checked: options.checked,
|
||||
relays: urls,
|
||||
indexers: indexerUrls,
|
||||
spaces: urls,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {neventEncode, npubEncode} from "nostr-tools/nip19"
|
||||
import {DAY, HOUR, MINUTE, WEEK, bech32ToHex, now} from "@welshman/lib"
|
||||
import {DAY, HOUR, MINUTE, WEEK, bech32ToHex, int, now} from "@welshman/lib"
|
||||
import {getLnUrl} from "@welshman/util"
|
||||
import {Profile} from "@welshman/domain"
|
||||
import type {Locator, Page} from "@playwright/test"
|
||||
|
|
@ -671,6 +671,46 @@ test("US-027a a permalink near the newest end lands at the bottom", async ({seed
|
|||
await expect(jumpToNewest(page)).toHaveCount(0)
|
||||
})
|
||||
|
||||
// A permalink pins the message it lands on, and the reader asking for the unread divider takes over.
|
||||
test("US-027b jump to the unread divider from a permalink", async ({seed, as}) => {
|
||||
let newest!: SeededEvent
|
||||
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.join(user.alice, "general")
|
||||
space.join(user.bob, "general")
|
||||
|
||||
// Deep enough that the jump centres what it lands on rather than clamping at the bottom.
|
||||
for (let i = 0; i < 120; i++) {
|
||||
space.message(user.bob, "general", `harbor watch note ${i}`, at(400 - i * 3, MINUTE))
|
||||
}
|
||||
|
||||
newest = space.message(user.bob, "general", "and the tide is with us", at(2, MINUTE))
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
const path = roomPath(url, "general")
|
||||
|
||||
// Alice has read none of this, so the divider is screens above the message she lands on.
|
||||
const page = await as(users.alice, `${path}?at=${newest.event.created_at}`, {
|
||||
checked: {[path]: now() - int(12, HOUR)},
|
||||
})
|
||||
|
||||
await expect(page.locator(`[data-event="${newest.id}"]`)).toBeInViewport()
|
||||
|
||||
const divider = page.locator("#new-messages")
|
||||
|
||||
await page.getByRole("button", {name: "New Messages"}).click()
|
||||
|
||||
await expect(divider).toBeInViewport()
|
||||
|
||||
// The pin is what would pull her back down, a frame or two after the scroll starts.
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(page.locator(`[data-event="${newest.id}"]`)).not.toBeInViewport()
|
||||
})
|
||||
|
||||
test("US-028 share a message somewhere else", async ({seed, as}) => {
|
||||
let shared!: SeededEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,11 @@
|
|||
isProgrammaticScroll = false
|
||||
}
|
||||
|
||||
const scrollToNewMessages = () => scrollToRow("new-messages", {behavior: "smooth"})
|
||||
// Asking for the divider takes the scroll position over, so a permalink's pin lets go of it.
|
||||
const scrollToNewMessages = () => {
|
||||
release()
|
||||
scrollToRow("new-messages", {behavior: "smooth"})
|
||||
}
|
||||
|
||||
// Dropping the anchor remounts the transcript on the present, which puts the reader at the live end.
|
||||
const scrollToBottom = () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue