0
2
# Notifications Design
## Overview
Add a notifications system to Grain, matching grain-next's approach: computed queries over existing records (favorites, comments, follows, mentions) with read/unread tracking via server-side timestamp.
## Notification Types
1. **gallery-favorite** — someone favorited your gallery
2. **gallery-comment** — someone commented on your gallery
3. **gallery-comment-mention** — someone mentioned you in a comment
4. **gallery-mention** — someone mentioned you in a gallery description
5. **reply** — someone replied to your comment
6. **follow** — someone followed you
## Data Layer
No new database tables. Notifications are computed by querying existing records where the target matches the viewer's DID:
- `social.grain.favorite` — `subject` is a gallery URI owned by viewer
- `social.grain.comment` — `subject` is a gallery URI owned by viewer, or facets contain a mention of viewer's DID
- `social.grain.comment` (reply) — `reply_to` references a comment by viewer
- `social.grain.graph.follow` — `subject` is viewer's DID
- `social.grain.gallery` — facets in description mention viewer's DID
### Read/Unread Tracking
Store a `lastSeenNotifications` ISO timestamp in `_preferences` via the existing `putPreference` endpoint. Notifications with `created_at` after this timestamp are "unseen."
## API
### New XRPC Query: `social.grain.unspecced.getNotifications`
- **Input:** viewer DID, cursor (optional), limit (default 20)
- **Output:** paginated edges with:
- `reason`: one of the 6 notification types
- `author`: hydrated profile (did, handle, displayName, avatar)
- `subject`: gallery URI or comment URI (context-dependent)
- `gallery`: hydrated gallery info (title, thumbnail) when applicable
- `commentText`: comment text when applicable
- `createdAt`: timestamp
- `cursor`: for pagination
- `unseenCount`: count of notifications newer than `lastSeenNotifications`
The query unions results from favorites, comments, follows, and galleries, sorted by `created_at` desc.
## UI
### Bell Icon
- **Desktop:** Left sidebar, between Home and Settings icons. Shows red badge with unseen count when > 0.
- **Mobile:** Bottom bar, between Create and Profile. Same badge behavior.
### Notifications Page (`/notifications`)
- Infinite scroll list, 20 items per page
- Each row: author avatar, action text, relative timestamp, context (gallery thumbnail, comment text)
- On mount: updates `lastSeenNotifications` to now, clears badge
### Notification Row Rendering by Type
| Type | Action Text | Context |
| ----------------------- | ---------------------------- | ----------------------------------------- |
| gallery-favorite | "favorited your gallery" | Gallery title + thumbnail |
| gallery-comment | "commented on your gallery" | Comment text + gallery thumbnail |
| gallery-comment-mention | "mentioned you in a comment" | Comment text + gallery thumbnail |
| gallery-mention | "mentioned you in a gallery" | Gallery description excerpt + thumbnail |
| reply | "replied to your comment" | Reply text + original comment + thumbnail |
| follow | "followed you" | (none) |
## State Management
### TanStack Query Keys
- `["notifications", did]` — paginated notification list (60s stale time)
- `["unseenNotificationCount", did]` — badge count (60s stale time)
### Mark-as-Read Flow
1. User navigates to `/notifications`
2. `onMount` calls `putPreference({ key: "lastSeenNotifications", value: now })`
3. Invalidates `["unseenNotificationCount"]` — badge clears
4. Optimistic update: set count to 0 immediately
## File Changes
### New Files
- `server/xrpc/getNotifications.ts` — XRPC query handler
- `app/routes/notifications/+page.svelte` — notifications page
- `app/lib/components/atoms/NotificationItem.svelte` — notification row component
### Modified Files
- `hatk.config.ts` — register new XRPC endpoint
- `app/lib/queries.ts` — notification list + unseen count queries
- `app/lib/components/organisms/Sidebar.svelte` — bell icon with badge
- `app/lib/components/molecules/MobileBottomBar.svelte` — bell icon with badge