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
- gallery-favorite — someone favorited your gallery
- gallery-comment — someone commented on your gallery
- gallery-comment-mention — someone mentioned you in a comment
- gallery-mention — someone mentioned you in a gallery description
- reply — someone replied to your comment
- 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—subjectis a gallery URI owned by viewersocial.grain.comment—subjectis a gallery URI owned by viewer, or facets contain a mention of viewer's DIDsocial.grain.comment(reply) —reply_toreferences a comment by viewersocial.grain.graph.follow—subjectis viewer's DIDsocial.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 typesauthor: hydrated profile (did, handle, displayName, avatar)subject: gallery URI or comment URI (context-dependent)gallery: hydrated gallery info (title, thumbnail) when applicablecommentText: comment text when applicablecreatedAt: timestampcursor: for paginationunseenCount: count of notifications newer thanlastSeenNotifications
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
lastSeenNotificationsto 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
- User navigates to
/notifications onMountcallsputPreference({ key: "lastSeenNotifications", value: now })- Invalidates
["unseenNotificationCount"]— badge clears - Optimistic update: set count to 0 immediately
File Changes
New Files
server/xrpc/getNotifications.ts— XRPC query handlerapp/routes/notifications/+page.svelte— notifications pageapp/lib/components/atoms/NotificationItem.svelte— notification row component
Modified Files
hatk.config.ts— register new XRPC endpointapp/lib/queries.ts— notification list + unseen count queriesapp/lib/components/organisms/Sidebar.svelte— bell icon with badgeapp/lib/components/molecules/MobileBottomBar.svelte— bell icon with badge