Show nav notification badges for activity under the current section (#404)
Co-authored-by: Coracle-Bot <npub1klq6260@nostr.local>
This commit is contained in:
parent
c02072863a
commit
4578644924
4 changed files with 78 additions and 3 deletions
|
|
@ -1538,6 +1538,21 @@ Acceptance:
|
|||
- An unresolvable nostr link redirects to the app's home rather than showing a
|
||||
broken page.
|
||||
|
||||
### US-110 — See another space's unread activity from a phone
|
||||
|
||||
As bob on a phone, I want the bottom bar's Spaces button to tell me another
|
||||
space wants attention, so that I don't have to leave the room I'm reading to
|
||||
find out.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- While bob has a room open, a message in a space he isn't in raises an unread
|
||||
dot on the Spaces button, matching the dot that space's row carries in
|
||||
`/spaces`.
|
||||
- A message in another room of the space he's already in does not; that space's
|
||||
own room list carries it.
|
||||
- Reading the other space takes the dot down.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Features the e2e suite cannot exercise, and what stops it.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ const settingRow = (page: Page, label: string) =>
|
|||
const spaceNavItem = (page: Page, name: string) =>
|
||||
page.locator(`.primary-nav [data-tip^="${name}"]`)
|
||||
|
||||
// The desktop rail and the phone's bottom bar each carry a link to the space list, and neither one
|
||||
// has an accessible name — both icons are masked svgs. Below tailwind's md breakpoint the rail is
|
||||
// display:none, so on a phone the visible one is the bar's.
|
||||
const spacesNavItem = (page: Page) => page.locator('a[href="/spaces"]:visible')
|
||||
|
||||
// The space menu's header, the one button in the secondary nav carrying the relay's address.
|
||||
const spaceMenu = (page: Page, url: string) =>
|
||||
page.locator(".secondary-nav").getByRole("button", {name: pattern(displayRelayUrl(url))})
|
||||
|
|
@ -418,3 +423,55 @@ test("US-107 open a nostr link", async ({seed, as}) => {
|
|||
|
||||
await expect(page).toHaveURL(/\/home$/)
|
||||
})
|
||||
|
||||
test("US-110 see another space's unread activity from a phone", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
const other = relay("other")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.room("random", {name: "Random"})
|
||||
other.room("general", {name: "General"})
|
||||
|
||||
space.join(user.alice, "general", "random")
|
||||
space.join(user.bob, "general", "random")
|
||||
other.join(user.alice, "general")
|
||||
other.join(user.bob, "general")
|
||||
})
|
||||
|
||||
const space = scenario.space("space")
|
||||
const other = scenario.space("other")
|
||||
|
||||
// A phone has no room list on screen while a room is open, so the bottom bar is the only place
|
||||
// activity elsewhere can surface
|
||||
const bob = await as(users.bob, roomPath(space.url, "general"), {
|
||||
context: {viewport: {width: 390, height: 844}, hasTouch: true},
|
||||
})
|
||||
|
||||
const spacesButton = spacesNavItem(bob)
|
||||
|
||||
await expect(spacesButton).toBeVisible()
|
||||
await expect(unreadDot(spacesButton)).toHaveCount(0)
|
||||
|
||||
const inOther = await as(users.alice, roomPath(other.url, "general"))
|
||||
|
||||
await send(inOther, "the server is on fire")
|
||||
|
||||
await expect(unreadDot(spacesButton)).toBeVisible()
|
||||
|
||||
// Meanwhile the space bob is sitting in gets a message too, in a room he isn't reading
|
||||
const inSpace = await as(users.alice, roomPath(space.url, "random"))
|
||||
|
||||
await send(inSpace, "anyone seen the sextant?")
|
||||
|
||||
await bob.goto(roomPath(other.url, "general"))
|
||||
|
||||
await expect(message(bob, "the server is on fire")).toBeVisible()
|
||||
|
||||
// Reading the other space empties the bar, even though bob's own space still has an unread room
|
||||
// — that one is the space's business, and its own indicators carry it
|
||||
await bob.goto(spacePath(space.url))
|
||||
|
||||
await expect(unreadDot(roomLink(bob, "Random"))).toBeVisible()
|
||||
await expect(unreadDot(spacesButton)).toHaveCount(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/env"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
let windowHeight = $state(0)
|
||||
|
||||
|
|
@ -17,7 +18,9 @@
|
|||
const navPadding = 8 * itemHeight
|
||||
const itemLimit = $derived(Math.max(0, (windowHeight - navPadding) / itemHeight))
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(p => $notifications.has(p)))
|
||||
const otherSpaceNotifications = $derived(
|
||||
secondarySpaceUrls.some(url => $notifications.has(makeSpacePath(url))),
|
||||
)
|
||||
|
||||
// Tippy mounts its content component once, so pass a stable reactive object it can read from
|
||||
const overflowProps = $state({urls: [] as string[]})
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
{#if onclick}
|
||||
<Button {onclick} data-tip={title} class={className}>
|
||||
{@render children?.()}
|
||||
{#if !active && notification}
|
||||
{#if notification}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
{:else}
|
||||
<Link {href} data-tip={title} class={className}>
|
||||
{@render children?.()}
|
||||
{#if !active && notification}
|
||||
{#if notification}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in a new issue