diff --git a/web/src/lib/components/timeline/NotificationsPreview.stories.svelte b/web/src/lib/components/timeline/NotificationsPreview.stories.svelte
index 047e321a5..1c78cd61f 100644
--- a/web/src/lib/components/timeline/NotificationsPreview.stories.svelte
+++ b/web/src/lib/components/timeline/NotificationsPreview.stories.svelte
@@ -1,5 +1,6 @@
-
-
+
+
+ ({ ...n, read: true }))
+ }}
+/>
+
+
+
diff --git a/web/src/lib/components/timeline/NotificationsPreview.svelte b/web/src/lib/components/timeline/NotificationsPreview.svelte
index 866cb352c..8976f38e7 100644
--- a/web/src/lib/components/timeline/NotificationsPreview.svelte
+++ b/web/src/lib/components/timeline/NotificationsPreview.svelte
@@ -4,14 +4,35 @@
import Bell from "$icon/bell";
import BellOff from "$icon/bell-off";
import Inbox from "$icon/inbox";
+ import { createLoad } from "$lib/action.svelte";
+ import NotificationList from "$lib/components/notifications/NotificationList.svelte";
+ import type { NotificationSummary } from "$lib/components/notifications/types";
+ import { getNotifications, type Notifications } from "$lib/notifications.svelte";
import PreviewCard from "./PreviewCard.svelte";
import PreviewEmpty from "./PreviewEmpty.svelte";
- // there is no notification item component yet, so this renders the two empty
- // states only. items land under the header once that exists.
- let { signedIn = false }: { signedIn?: boolean } = $props();
+ // the appview preview serves five rows and sends the rest to the full page
+ const PREVIEW_LIMIT = 5;
+ let {
+ signedIn = false,
+ notifications: fixtures
+ }: { signedIn?: boolean; notifications?: NotificationSummary[] } = $props();
+
+ // stories render this card without the layout, so the context can be absent
+ const context = getNotifications() as Notifications | undefined;
const path = "/notifications";
+
+ const preview = createLoad(async () => {
+ if (!signedIn) return null;
+ if (fixtures) {
+ return { notifications: fixtures, totalUnread: fixtures.filter((n) => !n.read).length };
+ }
+ return (await context?.preview(PREVIEW_LIMIT)) ?? null;
+ });
+
+ const items = $derived(preview.data?.notifications ?? []);
+ const totalUnread = $derived(preview.data?.totalUnread ?? null);
@@ -25,13 +46,19 @@
{/snippet}
- {#if signedIn}
+ {#if !signedIn}
+
+ Log in to view your inbox
+
+ {:else if items.length > 0}
+
+ {:else if totalUnread === 0}
All caught up!
{:else}
-
- Log in to view your inbox
+
+ Open your inbox
{/if}
diff --git a/web/src/lib/notifications.svelte.ts b/web/src/lib/notifications.svelte.ts
index 2f04a7267..2c78b61b2 100644
--- a/web/src/lib/notifications.svelte.ts
+++ b/web/src/lib/notifications.svelte.ts
@@ -1,15 +1,35 @@
import { getContext } from "svelte";
+import { createBobbinClient } from "$lib/api/client";
import { createDeliberiClient } from "$lib/api/deliberi";
-import { markEntityNotificationsRead } from "$lib/api/notifications";
+import {
+ getUnreadCount,
+ hydrateNotifications,
+ listNotifications,
+ markEntityNotificationsRead,
+ type NotificationItem
+} from "$lib/api/notifications";
+import type { NotificationSummary } from "$lib/components/notifications/types";
import type { Auth } from "$lib/auth.svelte";
export const NOTIFICATIONS_KEY = Symbol("notifications");
+export interface NotificationsPreview {
+ notifications: NotificationSummary[];
+ totalUnread: number;
+}
+
export interface Notifications {
markEntityVisited(entityUri: string): Promise;
+ /** latest rows plus the account-wide unread count, null when signed out
+ * or deliberi is unconfigured */
+ preview(limit: number): Promise;
}
-export const createNotifications = (auth: Auth, deliberiUrl: string | undefined): Notifications => {
+export const createNotifications = (
+ auth: Auth,
+ deliberiUrl: string | undefined,
+ bobbinUrl: string
+): Notifications => {
const markEntityVisited = async (entityUri: string): Promise => {
// yield first so the call lands outside the $effect that invoked it
await Promise.resolve();
@@ -19,7 +39,20 @@ export const createNotifications = (auth: Auth, deliberiUrl: string | undefined)
await markEntityNotificationsRead(deliberi, entityUri).catch(() => {});
};
- return { markEntityVisited };
+ const preview = async (limit: number): Promise => {
+ const agent = auth.agent;
+ if (!agent || !deliberiUrl) return null;
+ const deliberi = createDeliberiClient({ deliberiUrl, agent });
+ const hydrate = (items: NotificationItem[]): Promise =>
+ hydrateNotifications(createBobbinClient({ serviceUrl: bobbinUrl }), items);
+ const [countResult, listResult] = await Promise.all([
+ getUnreadCount(deliberi),
+ listNotifications(deliberi, { limit }).then((r) => hydrate(r.notifications))
+ ]);
+ return { notifications: listResult, totalUnread: countResult.count };
+ };
+
+ return { markEntityVisited, preview };
};
export const getNotifications = () => getContext(NOTIFICATIONS_KEY);
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index b5c03bdf9..f2f6f00fe 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -19,7 +19,9 @@
const auth = untrack(() => createAuth(data.publicConfig.bobbinUrl, data.auth));
setContext(AUTH_KEY, auth);
- const notifications = untrack(() => createNotifications(auth, data.publicConfig.deliberiUrl));
+ const notifications = untrack(() =>
+ createNotifications(auth, data.publicConfig.deliberiUrl, data.publicConfig.bobbinUrl)
+ );
setContext(NOTIFICATIONS_KEY, notifications);
onMount(() => {