diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md index 9d37834e..596e3088 100644 --- a/e2e/USER_STORIES.md +++ b/e2e/USER_STORIES.md @@ -1577,6 +1577,18 @@ Acceptance: - The thread list below the md breakpoint is a list of links rather than a table, and alice's thread carries a dot there; bob's own does not. +### US-114 — See which listings are unread + +As bob, I want the same dot on a board whose items are cards rather than rows, +so that every content section answers "which one is new" the same way. + +Acceptance: + +- A listing alice posted raises an unread dot on the Classifieds nav item; one + bob posted himself does not. +- Opening the list keeps the dot on the corner of alice's card, and bob's own + card carries none. + ## Out of scope Features the e2e suite cannot exercise, and what stops it. diff --git a/e2e/specs/notifications.spec.ts b/e2e/specs/notifications.spec.ts index 09e518c6..f326ab70 100644 --- a/e2e/specs/notifications.spec.ts +++ b/e2e/specs/notifications.spec.ts @@ -1,7 +1,7 @@ import {neventEncode, npubEncode} from "nostr-tools/nip19" import {HOUR, MINUTE} from "@welshman/lib" import {displayRelayUrl} from "@welshman/util" -import {MessagingRelayList, RelayList, Thread} from "@welshman/domain" +import {Classified, MessagingRelayList, RelayList, Thread} from "@welshman/domain" import type {Locator, Page} from "@playwright/test" import {expect, roomPath, spacePath, test, users} from "../harness" import type {SeededSpace, TestUser} from "../harness" @@ -481,6 +481,22 @@ test("US-110 see another space's unread activity from a phone", async ({seed, as const contentNavItem = (page: Page, name: string) => page.locator(".secondary-nav").getByRole("link", {name}) +const seedClassified = (space: SeededSpace, user: TestUser, title: string, createdAt: number) => + space.event( + user, + () => + space + .kind(Classified) + .writer() + .setRoom(space.url, "general") + .setIdentifier() + .setTitle(title) + .setPrice(100) + .setContent("in good condition") + .renderTemplate(), + createdAt, + ) + const seedThread = (space: SeededSpace, user: TestUser, title: string, createdAt: number) => space.event( user, @@ -579,3 +595,39 @@ test("US-113 see which threads are unread on a phone", async ({seed, as}) => { await expect(unreadDot(hers)).toBeVisible() await expect(unreadDot(his)).toHaveCount(0) }) + +// Classifieds stands in for the five boards whose items are cards rather than rows — they all +// render the same UnreadDot off the same content path, and only the corner it sits in differs. +test("US-114 see which listings are unread", async ({seed, as}) => { + 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") + + seedClassified(space, user.bob, "vintage sextant", at(3, HOUR)) + seedClassified(space, user.alice, "brass astrolabe", at(2, HOUR)) + }) + + const space = scenario.space("space") + const bob = await as(users.bob, roomPath(space.url, "general")) + + const classifiedsNav = contentNavItem(bob, "Classifieds") + + await expect(classifiedsNav).toBeVisible() + await expect(unreadDot(classifiedsNav)).toBeVisible() + + await classifiedsNav.click() + + const hers = bob.getByRole("link").filter({hasText: "brass astrolabe"}) + const his = bob.getByRole("link").filter({hasText: "vintage sextant"}) + + await expect(his).toBeVisible() + + // Same tick and throttle as US-112. + await bob.waitForTimeout(1500) + + await expect(unreadDot(hers)).toBeVisible() + await expect(unreadDot(his)).toHaveCount(0) +}) diff --git a/src/app/components/ArticleActions.svelte b/src/app/components/ArticleActions.svelte index 74fdbb01..3c59e64a 100644 --- a/src/app/components/ArticleActions.svelte +++ b/src/app/components/ArticleActions.svelte @@ -1,7 +1,6 @@ - + +
diff --git a/src/app/components/ClassifiedActions.svelte b/src/app/components/ClassifiedActions.svelte index 024ea480..f917cd5b 100644 --- a/src/app/components/ClassifiedActions.svelte +++ b/src/app/components/ClassifiedActions.svelte @@ -1,7 +1,6 @@ - + + {#if title}

diff --git a/src/app/components/CommentActions.svelte b/src/app/components/CommentActions.svelte index 60ef3487..505d9ee8 100644 --- a/src/app/components/CommentActions.svelte +++ b/src/app/components/CommentActions.svelte @@ -7,22 +7,18 @@ import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import EventActivity from "@app/components/EventActivity.svelte" import EventActions from "@app/components/EventActions.svelte" - import {makeSpacePath} from "@app/routes" type Props = { url: string event: TrustedEvent - segment?: string showActivity?: boolean context: FeedContext } - const {url, event, segment, showActivity = false, context}: Props = $props() + const {url, event, showActivity = false, context}: Props = $props() const h = tagValue(tagSpec("h"), event.tags) - const path = segment && makeSpacePath(url, segment, event.id) - const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h}) const createReaction = (values: EventContent) => publishReaction(event, values, {url, h}) @@ -38,8 +34,8 @@ {deleteReaction} {createReaction} reactionClass="tip-left" /> - {#if showActivity && path} - + {#if showActivity} + {/if} diff --git a/src/app/components/EventActivity.svelte b/src/app/components/EventActivity.svelte index 46b06c8b..06778672 100644 --- a/src/app/components/EventActivity.svelte +++ b/src/app/components/EventActivity.svelte @@ -1,22 +1,19 @@ - + +

{title}

@@ -22,7 +26,8 @@ class={cx("card card-sm card-interactive flex h-full w-full flex-col items-start gap-1 pr-10", { "card-primary": selected, })}> - {board.title() || "Untitled shelf"} + {board.title() || "Untitled shelf"} {#if board.description()} {board.description()} {/if} diff --git a/src/app/components/PollActions.svelte b/src/app/components/PollActions.svelte index 3a661974..adc335c2 100644 --- a/src/app/components/PollActions.svelte +++ b/src/app/components/PollActions.svelte @@ -9,7 +9,7 @@ import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import EventActivity from "@app/components/EventActivity.svelte" import EventActions from "@app/components/EventActions.svelte" - import {makePollPath, makeSpacePath} from "@app/routes" + import {makeSpacePath} from "@app/routes" type Props = { url: string @@ -22,7 +22,6 @@ const {url, event, showRoom, showActivity, context}: Props = $props() const h = tagValue(tagSpec("h"), event.tags) - const path = makePollPath(url, event.id) const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h}) @@ -44,7 +43,7 @@ {createReaction} reactionClass="tip-left" /> {#if showActivity} - + {/if} diff --git a/src/app/components/PollItem.svelte b/src/app/components/PollItem.svelte index 11708aab..379ff4d2 100644 --- a/src/app/components/PollItem.svelte +++ b/src/app/components/PollItem.svelte @@ -7,6 +7,7 @@ import type {FeedContext} from "@app/feeds" import RoomLink from "@app/components/RoomLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte" + import UnreadDot from "@app/components/UnreadDot.svelte" import {makePollPath} from "@app/routes" type Props = { @@ -17,12 +18,12 @@ const {url, event, context}: Props = $props() + const path = makePollPath(url, event.id) const h = tagValue(tagSpec("h"), event.tags) - + +
diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ThreadActions.svelte index 78600237..9ce8fefa 100644 --- a/src/app/components/ThreadActions.svelte +++ b/src/app/components/ThreadActions.svelte @@ -9,7 +9,7 @@ import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte" import EventActivity from "@app/components/EventActivity.svelte" import EventActions from "@app/components/EventActions.svelte" - import {makeThreadPath, makeSpacePath} from "@app/routes" + import {makeSpacePath} from "@app/routes" interface Props { url: string @@ -22,7 +22,6 @@ const {url, event, showRoom, showActivity, context}: Props = $props() const h = tagValue(tagSpec("h"), event.tags) - const path = makeThreadPath(url, event.id) const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url}) const createReaction = (values: EventContent) => publishReaction(event, values, {url}) @@ -43,7 +42,7 @@ {createReaction} reactionClass="tip-left" /> {#if showActivity} - + {/if} diff --git a/src/app/components/ThreadBoardItem.svelte b/src/app/components/ThreadBoardItem.svelte index fd30a9f9..36dd5655 100644 --- a/src/app/components/ThreadBoardItem.svelte +++ b/src/app/components/ThreadBoardItem.svelte @@ -4,12 +4,11 @@ import {filter, formatTimestamp, max, spec} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import {COMMENT, tagSpec, tagValue} from "@welshman/util" - import {fade} from "@lib/transition" import Link from "@lib/components/Link.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileName from "@app/components/ProfileName.svelte" + import UnreadDot from "@app/components/UnreadDot.svelte" import type {FeedContext} from "@app/feeds" - import {notifications} from "@app/notifications" import {makeThreadPath} from "@app/routes" type Props = { @@ -30,17 +29,13 @@ const onClick = () => goto(path) -{#snippet unread()} - {#if $notifications.has(path)} -
- {/if} -{/snippet} - {#if mobile} -

{@render unread()}{title || "Untitled thread"}

+

+ {title || "Untitled thread"} +

@@ -57,7 +52,7 @@ onclick={onClick} class="hover:bg-surface-less cursor-pointer border-b border-solid border-line text-sm transition-colors"> - {@render unread()}{title || "Untitled thread"} + {title || "Untitled thread"}
diff --git a/src/app/components/ThreadItem.svelte b/src/app/components/ThreadItem.svelte index 5b062efe..5db9ad19 100644 --- a/src/app/components/ThreadItem.svelte +++ b/src/app/components/ThreadItem.svelte @@ -8,6 +8,7 @@ import ThreadActions from "@app/components/ThreadActions.svelte" import type {FeedContext} from "@app/feeds" import RoomLink from "@app/components/RoomLink.svelte" + import UnreadDot from "@app/components/UnreadDot.svelte" import {makeThreadPath} from "@app/routes" type Props = { @@ -18,13 +19,13 @@ const {url, event, context}: Props = $props() + const path = makeThreadPath(url, event.id) const title = tagValue(tagSpec("title"), event.tags) const h = tagValue(tagSpec("h"), event.tags) - + + {#if title}

{title}

diff --git a/src/app/components/ThreadPost.svelte b/src/app/components/ThreadPost.svelte index 07679fe7..ae7163ac 100644 --- a/src/app/components/ThreadPost.svelte +++ b/src/app/components/ThreadPost.svelte @@ -92,7 +92,7 @@ Reply {#if isComment} - + {:else} {/if} diff --git a/src/app/components/UnreadDot.svelte b/src/app/components/UnreadDot.svelte new file mode 100644 index 00000000..ec4c0dcb --- /dev/null +++ b/src/app/components/UnreadDot.svelte @@ -0,0 +1,16 @@ + + +{#if $notifications.has(path)} +
+{/if} diff --git a/src/app/notifications.ts b/src/app/notifications.ts index 9c3e03a0..534e1b9b 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -1,18 +1,7 @@ import {derived, get, writable} from "svelte/store" import {Badge} from "@capawesome/capacitor-badge" import {page} from "$app/stores" -import { - assoc, - prop, - first, - identity, - groupBy, - now, - remove, - throttle, - parseJson, - gt, -} from "@welshman/lib" +import {assoc, first, identity, groupBy, now, remove, throttle, parseJson, gt} from "@welshman/lib" import type {SignedEvent, TrustedEvent} from "@welshman/util" import { getIdOrAddress, @@ -48,8 +37,6 @@ export const checked = withGetter( export const getChecked = (key: string) => checked.get()[key] -export const deriveChecked = (key: string) => derived(checked, prop(key)) - export const setChecked = (key: string) => checked.update(assoc(key, now())) /** Room path while video call UI hides chat; checked + badge stay active until chat is shown. */ diff --git a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte index 4ec7c3ed..c2854c57 100644 --- a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte @@ -101,7 +101,7 @@
- +
{/each} diff --git a/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte b/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte index 103ab993..6f9ed7fe 100644 --- a/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte @@ -89,7 +89,7 @@
- +
{/each} diff --git a/src/routes/spaces/[relay]/goals/[id]/+page.svelte b/src/routes/spaces/[relay]/goals/[id]/+page.svelte index a4812bb5..2b272bfa 100644 --- a/src/routes/spaces/[relay]/goals/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/goals/[id]/+page.svelte @@ -90,7 +90,7 @@
- +
{/each} diff --git a/src/routes/spaces/[relay]/polls/[id]/+page.svelte b/src/routes/spaces/[relay]/polls/[id]/+page.svelte index 491c1717..7d2d8381 100644 --- a/src/routes/spaces/[relay]/polls/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/polls/[id]/+page.svelte @@ -86,7 +86,7 @@
- +
{#if !showAll && $comments.length > 4} @@ -101,7 +101,7 @@
- +
{/each}