Reduce the home inbox to unread conversations and restyle the dashboard

This commit is contained in:
Coracle-Bot 2026-09-08 23:51:29 +00:00 committed by hodlbod
parent 7825477155
commit b50d35e5fa
7 changed files with 32 additions and 39 deletions

View file

@ -1563,15 +1563,14 @@ 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. It holds messages
only.
- The inbox lists each room and conversation with unread activity as a card,
naming the room and space and showing the latest message, newest first. It
holds messages only, and only while they are unread.
- 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.
- A conversation carries an unread dot, and "Mark all read" empties the inbox.
- Selecting a conversation opens it.
- Relay health checks are listed alongside the inbox, with the recommendation
each one applies.
- Hosting is offered whether or not she hosts a space: a shortcut to the hosting

View file

@ -405,17 +405,14 @@ test("US-116 read the home dashboard", async ({seed, as}) => {
// Hosting shows even though alice hosts nothing, as an invitation to start a space
await expect(page.getByRole("button", {name: "Start a space"})).toBeVisible()
await expect(conversation).toHaveAttribute("href", roomPath(space.url, "general"))
// The inbox is the badges, so a conversation leaves the list once it has nothing unread
await page.getByRole("button", {name: "Mark all read"}).click()
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(conversation).toHaveCount(0)
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()
await expect(page).toHaveURL(pattern(roomPath(space.url, "general")))
await expect(page.getByText("You're all caught up")).toBeVisible()
})
test("US-106 share text into the app", async ({seed, as}) => {

View file

@ -24,11 +24,17 @@
<Button class="button button-neutral button-xs" onclick={markAllRead}>Mark all read</Button>
{/if}
{/snippet}
{#if conversations.length === 0}
{#if conversations.length > 0}
<div class="flex flex-col gap-3 px-4 pb-4">
{#each conversations as conversation (conversation.path)}
<HomeInboxItem {conversation} />
{/each}
</div>
{:else}
<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="font-medium">You're all caught up</p>
<p class="max-w-md text-sm opacity-75">
The rooms, space chats and direct messages you belong to show their latest message here.
The rooms, space chats and direct messages you belong to show what's unread here.
</p>
<div class="flex flex-wrap justify-center gap-2">
<Link href="/spaces" class="button button-primary button-sm">
@ -41,11 +47,5 @@
</Button>
</div>
</div>
{:else}
<div class="flex flex-col divide-y divide-line border-t border-line">
{#each conversations as conversation (conversation.path)}
<HomeInboxItem {conversation} />
{/each}
</div>
{/if}
</HomeSection>

View file

@ -9,10 +9,10 @@
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import UnreadDot from "@app/components/UnreadDot.svelte"
import {user} from "@app/core"
import type {InboxConversation} from "@app/inbox"
import type {Activity} from "@app/notifications"
type Props = {
conversation: InboxConversation
conversation: Activity
}
const {conversation}: Props = $props()
@ -24,7 +24,7 @@
const others = $derived(uniq(remove($user.pubkey, conversation.pubkeys ?? [])))
</script>
<Link href={path} class="flex items-center gap-3 px-4 py-3 hover:bg-surface-more">
<Link href={path} class="card card-sm card-interactive flex items-center gap-3">
{#if url}
<RelayIcon {url} size={9} class="shrink-0" />
{:else}

View file

@ -14,8 +14,8 @@
<section class="flex min-w-0 flex-col border-b border-line last:border-b-0">
<div class="flex items-center justify-between gap-3 px-4 pt-4 pb-3">
<h2 class="flex items-center gap-2 text-xs font-bold tracking-widest uppercase opacity-75">
<Icon {icon} size={4} />
<h2 class="flex items-center gap-2 text-base font-bold tracking-wide uppercase">
<Icon {icon} size={5} />
{title}
</h2>
{@render action?.()}

View file

@ -1,20 +1,17 @@
import {derived} from "svelte/store"
import {sortBy} from "@welshman/lib"
import type {Activity} from "@app/notifications"
import {allNotifications, latestActivityByPath} from "@app/notifications"
export type InboxConversation = Activity & {unread: boolean}
// Every room, space chat and direct message the user can see, newest first, whether or not it
// has anything unread - an inbox that hid what has been read would be empty for anyone caught up.
// Every room, space chat and direct message with something unread, newest first. The inbox shows
// exactly what the badges show, so reading a conversation is what takes it out of the list.
export const inboxConversations = derived(
[latestActivityByPath, allNotifications],
([$latestActivityByPath, $allNotifications]) =>
sortBy(
conversation => -conversation.event.created_at,
[...$latestActivityByPath.values()]
.filter(activity => !activity.contentKind)
.map(activity => ({...activity, unread: $allNotifications.has(activity.path)})),
activity => -activity.event.created_at,
[...$latestActivityByPath.values()].filter(
activity => !activity.contentKind && $allNotifications.has(activity.path),
),
),
)

View file

@ -38,9 +38,9 @@
<span class="hidden text-sm opacity-75 sm:inline">{today}</span>
</div>
</PageBar>
<PageContent noPad class="flex flex-col">
<PageContent noPad class="flex flex-col bg-surface">
<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">
<div class="flex min-w-0 flex-col lg:flex-[2] lg:border-r lg:border-line">
<HomeInbox />
<HomeActivity />
<HomeNetwork />