Show which item raised an unread dot on every content board (#419)
Co-authored-by: Coracle-Bot <npub1klq6260@nostr.local>
This commit is contained in:
parent
4d7b052724
commit
bec000b05c
25 changed files with 142 additions and 89 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {uniq} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {Article} from "@welshman/domain"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
|
|
@ -14,7 +13,7 @@
|
|||
import EventActionBar from "@app/components/EventActionBar.svelte"
|
||||
import {reader} from "@app/core"
|
||||
import {deriveIsDeleted} from "@app/repository"
|
||||
import {makeArticlePath, makeSpacePath} from "@app/routes"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -31,7 +30,6 @@
|
|||
const article = $derived(reader(Article)(event))
|
||||
const h = $derived(article.room())
|
||||
const topics = $derived(article.topics())
|
||||
const path = $derived(makeArticlePath(url, getAddress(event)))
|
||||
const deleted = $derived(deriveIsDeleted(event))
|
||||
|
||||
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})
|
||||
|
|
@ -54,7 +52,7 @@
|
|||
<EventActionBar {url} {event} noun="Article">
|
||||
{#snippet leading()}
|
||||
{#if showActivity}
|
||||
<EventActivity {path} {event} {context} size="sm" hideLastActive />
|
||||
<EventActivity {event} {context} size="sm" hideLastActive />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActionBar>
|
||||
|
|
@ -84,7 +82,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ArticleActions from "@app/components/ArticleActions.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {reader} from "@app/core"
|
||||
import {makeArticlePath} from "@app/routes"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
|
||||
const {url, event, context}: Props = $props()
|
||||
|
||||
const path = $derived(makeArticlePath(url, getAddress(event)))
|
||||
const article = $derived(reader(Article)(event))
|
||||
const title = $derived(article.title())
|
||||
const summary = $derived(article.summary())
|
||||
|
|
@ -28,10 +30,8 @@
|
|||
<div data-component="ArticleItem" class="cv relative w-full card card-interactive">
|
||||
<!-- An overlay rather than a wrapper: the card carries a profile button and the room and action
|
||||
links, and none of those may sit inside an anchor. -->
|
||||
<Link
|
||||
class="absolute inset-0 rounded-2xl"
|
||||
href={makeArticlePath(url, getAddress(event))}
|
||||
aria-label={title || "Untitled"} />
|
||||
<Link class="absolute inset-0 rounded-2xl" href={path} aria-label={title || "Untitled"} />
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
<div class="pointer-events-none relative flex flex-col gap-2">
|
||||
{#if image}
|
||||
<img src={image} alt="" class="h-40 w-full rounded-2xl object-cover" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getAddress, tagSpec, tagValue} from "@welshman/util"
|
||||
import {tagSpec, tagValue} from "@welshman/util"
|
||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import {user} from "@app/core"
|
||||
import {makeCalendarPath, makeSpacePath} from "@app/routes"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -28,7 +28,6 @@
|
|||
const {url, event, showRoom, showActivity, context}: Props = $props()
|
||||
|
||||
const h = $derived(tagValue(tagSpec("h"), event.tags))
|
||||
const path = $derived(makeCalendarPath(url, getAddress(event)))
|
||||
|
||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||
|
||||
|
|
@ -52,7 +51,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import {makeCalendarPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -17,12 +18,12 @@
|
|||
|
||||
const {url, event, context}: Props = $props()
|
||||
|
||||
const path = $derived(makeCalendarPath(url, getAddress(event)))
|
||||
const h = $derived(tagValue(tagSpec("h"), event.tags))
|
||||
</script>
|
||||
|
||||
<Link
|
||||
class="cv flex flex-col gap-3 card card-interactive w-full"
|
||||
href={makeCalendarPath(url, getAddress(event))}>
|
||||
<Link class="cv relative flex flex-col gap-3 card card-interactive w-full" href={path}>
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
<CalendarEventHeader {event} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {uniq} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {Classified} from "@welshman/domain"
|
||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
import {normalizeTopic} from "@lib/util"
|
||||
|
|
@ -18,7 +17,7 @@
|
|||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import ClassifiedEdit from "@app/components/ClassifiedEdit.svelte"
|
||||
import {reader, user} from "@app/core"
|
||||
import {makeClassifiedPath, makeSpacePath} from "@app/routes"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -36,7 +35,6 @@
|
|||
const classified = $derived(reader(Classified)(event))
|
||||
const h = $derived(classified.room())
|
||||
const topics = $derived(classified.topics() ?? [])
|
||||
const path = $derived(makeClassifiedPath(url, getAddress(event)))
|
||||
|
||||
const editClassified = () => pushModal(ClassifiedEdit, {url, event})
|
||||
|
||||
|
|
@ -70,7 +68,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Listing">
|
||||
{#snippet customActions()}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import {makeClassifiedPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
|
||||
const {url, event, context}: Props = $props()
|
||||
|
||||
const path = $derived(makeClassifiedPath(url, getAddress(event)))
|
||||
const classified = $derived(reader(Classified)(event))
|
||||
const title = $derived(classified.title())
|
||||
const h = $derived(classified.room())
|
||||
|
|
@ -29,9 +31,8 @@
|
|||
const price = $derived(classified.price())
|
||||
</script>
|
||||
|
||||
<Link
|
||||
class="cv flex flex-col gap-2 card card-interactive w-full"
|
||||
href={makeClassifiedPath(url, getAddress(event))}>
|
||||
<Link class="cv relative flex flex-col gap-2 card card-interactive w-full" href={path}>
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
{#if title}
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p class="text-xl">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<EventActivity {path} {event} {context} />
|
||||
{#if showActivity}
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {filter, max, gt, spec, formatTimestampRelative} from "@welshman/lib"
|
||||
import {filter, max, spec, formatTimestampRelative} from "@welshman/lib"
|
||||
import {COMMENT} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {deriveChecked} from "@app/notifications"
|
||||
|
||||
// hideLastActive: redundant next to a detail page's own comments below.
|
||||
const {
|
||||
path,
|
||||
event,
|
||||
context,
|
||||
size = "xs",
|
||||
hideLastActive,
|
||||
}: {
|
||||
path: string
|
||||
event: TrustedEvent
|
||||
context: FeedContext
|
||||
size?: "xs" | "sm"
|
||||
|
|
@ -25,7 +22,6 @@
|
|||
|
||||
const buttonClass = `button button-neutral button-${size} rounded-full`
|
||||
|
||||
const checked = deriveChecked(path)
|
||||
const related = context.related(event)
|
||||
const replies = derived(related, $related => filter(spec({kind: COMMENT}), $related))
|
||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||
|
|
@ -36,10 +32,7 @@
|
|||
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||
</div>
|
||||
{#if !hideLastActive}
|
||||
<div class="{buttonClass} relative">
|
||||
{#if gt(lastActive, $checked)}
|
||||
<div class="h-2 w-2 rounded-full bg-primary text-primary-content"></div>
|
||||
{/if}
|
||||
<div class={buttonClass}>
|
||||
Active {formatTimestampRelative(lastActive)}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import EventActivity from "@app/components/EventActivity.svelte"
|
||||
import EventActions from "@app/components/EventActions.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeGoalPath, 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 path = makeGoalPath(url, event.id)
|
||||
const goal = reader(ZapGoal)(event)
|
||||
|
||||
const h = goal.room()
|
||||
|
|
@ -47,7 +46,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import type {FeedContext} from "@app/feeds"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import {makeGoalPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -21,14 +22,14 @@
|
|||
|
||||
const goal = reader(ZapGoal)(event)
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
const title = goal.title()
|
||||
const summary = goal.summary()
|
||||
const h = goal.room()
|
||||
</script>
|
||||
|
||||
<Link
|
||||
class="cv flex flex-col gap-2 card card-interactive w-full"
|
||||
href={makeGoalPath(url, event.id)}>
|
||||
<Link class="cv relative flex flex-col gap-2 card card-interactive w-full" href={path}>
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
<p class="text-2xl">{title}</p>
|
||||
<Content
|
||||
event={{content: summary, tags: event.tags}}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
import Button from "@lib/components/Button.svelte"
|
||||
import MenuButton from "@lib/components/MenuButton.svelte"
|
||||
import BoardMenu from "@app/components/BoardMenu.svelte"
|
||||
import UnreadDot from "@app/components/UnreadDot.svelte"
|
||||
import {makeLibraryPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -13,6 +15,8 @@
|
|||
}
|
||||
|
||||
const {url, board, selected, onclick}: Props = $props()
|
||||
|
||||
const path = $derived(makeLibraryPath(url, board.address()))
|
||||
</script>
|
||||
|
||||
<div class="relative w-56 shrink-0">
|
||||
|
|
@ -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,
|
||||
})}>
|
||||
<strong class="truncate w-full text-left">{board.title() || "Untitled shelf"}</strong>
|
||||
<strong class="truncate w-full text-left"
|
||||
><UnreadDot {path} class="mr-1" />{board.title() || "Untitled shelf"}</strong>
|
||||
{#if board.description()}
|
||||
<span class="line-clamp-2 text-left text-sm opacity-70">{board.description()}</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Poll" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
</script>
|
||||
|
||||
<Link
|
||||
class="cv flex flex-col gap-2 card card-interactive w-full"
|
||||
href={makePollPath(url, event.id)}>
|
||||
<Link class="cv relative flex flex-col gap-2 card card-interactive w-full" href={path}>
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
<NoteContent {event} {url} />
|
||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<EventActivity {path} {event} {context} />
|
||||
<EventActivity {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
</script>
|
||||
|
||||
{#snippet unread()}
|
||||
{#if $notifications.has(path)}
|
||||
<div class="mr-1 inline-block h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if mobile}
|
||||
<Link
|
||||
href={path}
|
||||
class="cv hover:bg-surface-less flex w-full flex-col gap-2 border-b border-solid border-line px-4 py-3 text-left text-sm transition-colors">
|
||||
<p class="truncate font-medium">{@render unread()}{title || "Untitled thread"}</p>
|
||||
<p class="truncate font-medium">
|
||||
<UnreadDot {path} class="mr-1" />{title || "Untitled thread"}
|
||||
</p>
|
||||
<div class="text-content-muted flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<ProfileCircle pubkey={event.pubkey} {url} size={4} />
|
||||
|
|
@ -57,7 +52,7 @@
|
|||
onclick={onClick}
|
||||
class="hover:bg-surface-less cursor-pointer border-b border-solid border-line text-sm transition-colors">
|
||||
<td class="max-w-0 truncate px-4 py-3 align-top">
|
||||
{@render unread()}{title || "Untitled thread"}
|
||||
<UnreadDot {path} class="mr-1" />{title || "Untitled thread"}
|
||||
</td>
|
||||
<td class="w-32 px-4 py-3 align-middle">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
</script>
|
||||
|
||||
<Link
|
||||
class="cv flex flex-col gap-2 card card-interactive w-full"
|
||||
href={makeThreadPath(url, event.id)}>
|
||||
<Link class="cv relative flex flex-col gap-2 card card-interactive w-full" href={path}>
|
||||
<UnreadDot {path} class="absolute right-3 top-3" />
|
||||
{#if title}
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p class="text-xl">{title}</p>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
Reply
|
||||
</Button>
|
||||
{#if isComment}
|
||||
<CommentActions segment="threads" {event} {url} {context} />
|
||||
<CommentActions {event} {url} {context} />
|
||||
{:else}
|
||||
<ThreadActions {event} {url} {context} />
|
||||
{/if}
|
||||
|
|
|
|||
16
src/app/components/UnreadDot.svelte
Normal file
16
src/app/components/UnreadDot.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {fade} from "@lib/transition"
|
||||
import {notifications} from "@app/notifications"
|
||||
|
||||
type Props = {
|
||||
path: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {path, class: className}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if $notifications.has(path)}
|
||||
<div class={cx("inline-block h-2 w-2 rounded-full bg-primary", className)} transition:fade></div>
|
||||
{/if}
|
||||
|
|
@ -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<number>(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. */
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
<NoteCard event={reply} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={reply} {url} />
|
||||
<CommentActions segment="calendar" event={reply} {url} {context} />
|
||||
<CommentActions event={reply} {url} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
<NoteCard event={reply} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={reply} {url} />
|
||||
<CommentActions segment="classifieds" event={reply} {url} {context} />
|
||||
<CommentActions event={reply} {url} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
<NoteCard event={reply} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={reply} {url} />
|
||||
<CommentActions segment="goals" event={reply} {url} {context} />
|
||||
<CommentActions event={reply} {url} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
<NoteCard event={$event} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12 flex flex-col gap-3">
|
||||
<NoteContent showEntire event={$event} {url} />
|
||||
<CommentActions segment="polls" showActivity {url} event={$event} {context} />
|
||||
<CommentActions showActivity {url} event={$event} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{#if !showAll && $comments.length > 4}
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<NoteCard event={reply} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={reply} {url} />
|
||||
<CommentActions segment="polls" event={reply} {url} {context} />
|
||||
<CommentActions event={reply} {url} {context} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Reference in a new issue