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: Acceptance:
- The inbox lists each room and conversation with unread activity, naming the - The inbox lists each room and conversation with unread activity as a card,
room and space and showing the latest message, newest first. It holds messages naming the room and space and showing the latest message, newest first. It
only. holds messages only, and only while they are unread.
- Activity is a row of one card per space, counting what that space has waiting - 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 that isn't a message - threads, events, classifieds and the rest. A card
disappears once its space is read. disappears once its space is read.
- A conversation with unread activity carries an unread dot, and "Mark all read" - A conversation carries an unread dot, and "Mark all read" empties the inbox.
clears every dot on the page. - Selecting a conversation opens it.
- Selecting a conversation opens it, and it stays in the inbox once read.
- Relay health checks are listed alongside the inbox, with the recommendation - Relay health checks are listed alongside the inbox, with the recommendation
each one applies. each one applies.
- Hosting is offered whether or not she hosts a space: a shortcut to the hosting - 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 // 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(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 page.getByRole("button", {name: "Mark all read"}).click()
await expect(unreadDot(conversation)).toHaveCount(0) await expect(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) await expect(activity).toHaveCount(0)
await expect(page.getByText("You're all caught up")).toBeVisible()
// 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")))
}) })
test("US-106 share text into the app", async ({seed, as}) => { 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> <Button class="button button-neutral button-xs" onclick={markAllRead}>Mark all read</Button>
{/if} {/if}
{/snippet} {/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"> <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"> <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> </p>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-2">
<Link href="/spaces" class="button button-primary button-sm"> <Link href="/spaces" class="button button-primary button-sm">
@ -41,11 +47,5 @@
</Button> </Button>
</div> </div>
</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} {/if}
</HomeSection> </HomeSection>

View file

@ -9,10 +9,10 @@
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import UnreadDot from "@app/components/UnreadDot.svelte" import UnreadDot from "@app/components/UnreadDot.svelte"
import {user} from "@app/core" import {user} from "@app/core"
import type {InboxConversation} from "@app/inbox" import type {Activity} from "@app/notifications"
type Props = { type Props = {
conversation: InboxConversation conversation: Activity
} }
const {conversation}: Props = $props() const {conversation}: Props = $props()
@ -24,7 +24,7 @@
const others = $derived(uniq(remove($user.pubkey, conversation.pubkeys ?? []))) const others = $derived(uniq(remove($user.pubkey, conversation.pubkeys ?? [])))
</script> </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} {#if url}
<RelayIcon {url} size={9} class="shrink-0" /> <RelayIcon {url} size={9} class="shrink-0" />
{:else} {:else}

View file

@ -14,8 +14,8 @@
<section class="flex min-w-0 flex-col border-b border-line last:border-b-0"> <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"> <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"> <h2 class="flex items-center gap-2 text-base font-bold tracking-wide uppercase">
<Icon {icon} size={4} /> <Icon {icon} size={5} />
{title} {title}
</h2> </h2>
{@render action?.()} {@render action?.()}

View file

@ -1,20 +1,17 @@
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {sortBy} from "@welshman/lib" import {sortBy} from "@welshman/lib"
import type {Activity} from "@app/notifications"
import {allNotifications, latestActivityByPath} from "@app/notifications" import {allNotifications, latestActivityByPath} from "@app/notifications"
export type InboxConversation = Activity & {unread: boolean} // 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.
// 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.
export const inboxConversations = derived( export const inboxConversations = derived(
[latestActivityByPath, allNotifications], [latestActivityByPath, allNotifications],
([$latestActivityByPath, $allNotifications]) => ([$latestActivityByPath, $allNotifications]) =>
sortBy( sortBy(
conversation => -conversation.event.created_at, activity => -activity.event.created_at,
[...$latestActivityByPath.values()] [...$latestActivityByPath.values()].filter(
.filter(activity => !activity.contentKind) activity => !activity.contentKind && $allNotifications.has(activity.path),
.map(activity => ({...activity, unread: $allNotifications.has(activity.path)})), ),
), ),
) )

View file

@ -38,9 +38,9 @@
<span class="hidden text-sm opacity-75 sm:inline">{today}</span> <span class="hidden text-sm opacity-75 sm:inline">{today}</span>
</div> </div>
</PageBar> </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 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 /> <HomeInbox />
<HomeActivity /> <HomeActivity />
<HomeNetwork /> <HomeNetwork />