Split space activity out of the home inbox into its own section

This commit is contained in:
Coracle-Bot 2026-09-08 23:47:49 +00:00 committed by hodlbod
parent a3d9106a43
commit 7825477155
5 changed files with 70 additions and 27 deletions

View file

@ -1564,7 +1564,11 @@ that I don't have to walk every space to find out.
Acceptance:
- The inbox lists each room and conversation with unread activity, naming the
room and space and showing the latest message, newest first.
room and space and showing the latest message, newest first. It holds messages
only.
- Activity is a row of one card per space, counting what that space has waiting
that isn't a message - threads, events, classifieds and the rest. A card
disappears once its space is read.
- A conversation with unread activity carries an unread dot, and "Mark all read"
clears every dot on the page.
- Selecting a conversation opens it, and it stays in the inbox once read.

View file

@ -366,6 +366,19 @@ test("US-116 read the home dashboard", async ({seed, as}) => {
space.join(user.alice, "general")
space.join(user.bob, "general")
space.message(user.bob, "general", "the server is on fire", at(1, HOUR))
space.event(
user.bob,
() =>
space
.kind(Thread)
.writer()
.setRoom(space.url, "general")
.setTitle("Bed rotation")
.setContent("How often do you move things around?")
.renderTemplate(),
at(2, HOUR),
)
})
const space = scenario.space("space")
@ -378,6 +391,14 @@ test("US-116 read the home dashboard", async ({seed, as}) => {
await expect(conversation).toContainText("General")
await expect(unreadDot(conversation)).toBeVisible()
// A space's threads, events and classifieds are counted per space in Activity rather than listed
// as conversations, so the inbox stays a list of messages
const activity = page.getByRole("link").filter({hasText: "1 thread"})
await expect(page.getByRole("heading", {name: "Activity"})).toBeVisible()
await expect(activity).toBeVisible()
await expect(conversation).not.toContainText("1 thread")
// Relay health checks had no mount point at all before the dashboard
await expect(page.getByText("Health checks")).toBeVisible()
@ -388,6 +409,9 @@ test("US-116 read the home dashboard", async ({seed, as}) => {
await expect(unreadDot(conversation)).toHaveCount(0)
// Unlike a conversation, a space's activity card is a count of what's new, so reading it empties
await expect(activity).toHaveCount(0)
// A conversation stays in the inbox once it's read - it's a list of where things are, not a queue
await conversation.click()

View file

@ -0,0 +1,34 @@
<script lang="ts">
import {formatTimestamp} from "@welshman/lib"
import Feed from "@assets/icons/feed.svg?dataurl"
import Link from "@lib/components/Link.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {displayContentCount} from "@app/content"
import {inboxSpaceContent} from "@app/inbox"
import {makeSpacePath} from "@app/routes"
</script>
{#if $inboxSpaceContent.length > 0}
<HomeSection title="Activity" icon={Feed}>
<div class="scroll-container flex items-stretch gap-3 overflow-x-auto px-4 pb-4">
{#each $inboxSpaceContent as { url, timestamp, countsByKind } (url)}
<Link
href={makeSpacePath(url)}
class="card card-sm card-interactive flex w-56 shrink-0 flex-col gap-2">
<div class="flex min-w-0 items-center gap-2">
<RelayIcon {url} size={8} class="shrink-0" />
<strong class="min-w-0 flex-1 truncate"><RelayName {url} /></strong>
</div>
<div class="flex flex-col text-sm opacity-75">
{#each [...countsByKind] as [kind, count] (kind)}
<span>{displayContentCount(kind, count)}</span>
{/each}
</div>
<span class="mt-auto text-xs opacity-50">{formatTimestamp(timestamp)}</span>
</Link>
{/each}
</div>
</HomeSection>
{/if}

View file

@ -7,23 +7,12 @@
import Button from "@lib/components/Button.svelte"
import HomeSection from "@app/components/HomeSection.svelte"
import HomeInboxItem from "@app/components/HomeInboxItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {displayContentCount} from "@app/content"
import {inboxConversations, inboxSpaceContent} from "@app/inbox"
import {inboxConversations} from "@app/inbox"
import {navigate} from "@app/modal"
import {setChecked} from "@app/notifications"
import {makeSpacePath} from "@app/routes"
import {allNotifications, setChecked} from "@app/notifications"
const conversations = $derived($inboxConversations.slice(0, 8))
const hasUnread = $derived(
$inboxConversations.some(conversation => conversation.unread) || $inboxSpaceContent.length > 0,
)
const displayContent = (countsByKind: Map<number, number>) =>
[...countsByKind].map(([kind, count]) => displayContentCount(kind, count)).join(" · ")
const markAllRead = () => setChecked("*")
const startChat = () => navigate("/chat")
@ -31,16 +20,15 @@
<HomeSection title="Inbox" icon={Inbox}>
{#snippet action()}
{#if hasUnread}
{#if $allNotifications.size > 0}
<Button class="button button-neutral button-xs" onclick={markAllRead}>Mark all read</Button>
{/if}
{/snippet}
{#if conversations.length === 0 && $inboxSpaceContent.length === 0}
{#if conversations.length === 0}
<div class="flex flex-col items-center gap-3 px-4 pb-8 text-center">
<p class="font-medium">Nothing in your inbox yet</p>
<p class="max-w-md text-sm opacity-75">
Rooms and spaces you belong to report their activity here — messages, threads, classifieds,
events and polls.
The rooms, space chats and direct messages you belong to show their latest message here.
</p>
<div class="flex flex-wrap justify-center gap-2">
<Link href="/spaces" class="button button-primary button-sm">
@ -58,15 +46,6 @@
{#each conversations as conversation (conversation.path)}
<HomeInboxItem {conversation} />
{/each}
{#each $inboxSpaceContent as { url, countsByKind } (url)}
<Link
href={makeSpacePath(url)}
class="flex items-center gap-3 px-4 py-3 text-sm hover:bg-surface-more">
<RelayIcon {url} size={6} class="shrink-0" />
<strong class="truncate"><RelayName {url} /></strong>
<span class="truncate opacity-75">{displayContent(countsByKind)}</span>
</Link>
{/each}
</div>
{/if}
</HomeSection>

View file

@ -6,6 +6,7 @@
import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import HomeInbox from "@app/components/HomeInbox.svelte"
import HomeActivity from "@app/components/HomeActivity.svelte"
import HomeNetwork from "@app/components/HomeNetwork.svelte"
import HomeHealthChecks from "@app/components/HomeHealthChecks.svelte"
import HomeHosting from "@app/components/HomeHosting.svelte"
@ -41,6 +42,7 @@
<div class="flex min-w-0 flex-col lg:flex-row lg:items-stretch">
<div class="flex min-w-0 flex-col bg-surface lg:flex-[2] lg:border-r lg:border-line">
<HomeInbox />
<HomeActivity />
<HomeNetwork />
</div>
<div class="flex min-w-0 flex-col border-t border-line lg:w-80 lg:shrink-0 lg:border-t-0">