Notification API
Badge updates, pings, and read state management.
Overview
On Book Pro's notification system provides:
- Badge counts in sidebar navigation
- Real-time pings for @mentions and announcements
- Read/unread state tracking per user
Data Model
typescript
interface Notification {
id: string;
projectId: string;
recipientId: string; // User ID of recipient
type: NotificationType;
title: string;
body: string;
link?: string; // Deep link to related content
read: boolean;
createdAt: Timestamp;
}
type NotificationType =
| 'mention' // @mentioned in a post
| 'announcement' // Pinned announcement
| 'deadline' // Upcoming deadline reminder
| 'invitation' // Project invitation
| 'schedule_change'; // Rehearsal time changedCreating Notifications
Server-Side (Cloud Functions)
typescript
// functions/src/notifications.ts
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
// Auto-notify on @mention
export const onMention = onDocumentCreated(
'projects/{projectId}/posts/{postId}',
async (event) => {
const post = event.data?.data();
if (!post) return;
// Extract @mentions from content
const mentions = extractMentions(post.content);
for (const userId of mentions) {
await createNotification({
projectId: event.params.projectId,
recipientId: userId,
type: 'mention',
title: `${post.authorName} mentioned you`,
body: truncate(post.content, 100),
link: `/feed/${event.params.postId}`,
});
}
}
);
const createNotification = async (data: Omit<Notification, 'id' | 'read' | 'createdAt'>) => {
const notification: Notification = {
...data,
id: crypto.randomUUID(),
read: false,
createdAt: Timestamp.now(),
};
await db.collection('notifications').doc(notification.id).set(notification);
};Client-Side (Direct Write)
typescript
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
export const sendNotification = async (
recipientId: string,
type: NotificationType,
title: string,
body: string,
link?: string
) => {
const projectId = useAppStore.getState().meta.projectId;
await addDoc(collection(db, 'notifications'), {
projectId,
recipientId,
type,
title,
body,
link,
read: false,
createdAt: serverTimestamp(),
});
};Real-Time Badge Updates
Subscribe to Unread Count
typescript
import { collection, query, where, onSnapshot } from 'firebase/firestore';
export const useUnreadCount = () => {
const [count, setCount] = useState(0);
const userId = useAppStore((s) => s.auth.user?.uid);
useEffect(() => {
if (!userId) return;
const q = query(
collection(db, 'notifications'),
where('recipientId', '==', userId),
where('read', '==', false)
);
const unsubscribe = onSnapshot(q, (snapshot) => {
setCount(snapshot.size);
});
return unsubscribe;
}, [userId]);
return count;
};Display Badge
tsx
import { NotificationBadge } from '@/components/common';
export const SidebarNotifications = () => {
const count = useUnreadCount();
return (
<div className="relative">
<Icon name="notifications" />
{count > 0 && <NotificationBadge count={count} />}
</div>
);
};Read State Management
Mark Single as Read
typescript
export const markAsRead = async (notificationId: string) => {
await updateDoc(doc(db, 'notifications', notificationId), {
read: true,
});
};Mark All as Read
typescript
export const markAllAsRead = async (userId: string) => {
const q = query(
collection(db, 'notifications'),
where('recipientId', '==', userId),
where('read', '==', false)
);
const snapshot = await getDocs(q);
const batch = writeBatch(db);
snapshot.docs.forEach((doc) => {
batch.update(doc.ref, { read: true });
});
await batch.commit();
};Auto-Mark on View
tsx
export const NotificationItem = ({ notification }: Props) => {
useEffect(() => {
if (!notification.read) {
markAsRead(notification.id);
}
}, [notification.id, notification.read]);
return (
<div className={notification.read ? 'opacity-60' : ''}>
<h4>{notification.title}</h4>
<p>{notification.body}</p>
</div>
);
};Notification Types
| Type | Trigger | Default Behavior |
|---|---|---|
mention | @mention in post | Badge + list |
announcement | Pinned post created | Badge + toast |
deadline | 24h before deadline | Badge + list |
invitation | Added to project | Badge + modal |
schedule_change | Event time modified | Badge + toast |
Store Integration
typescript
// src/store/notification-slice.ts
interface NotificationState {
notifications: Notification[];
unreadCount: number;
}
interface NotificationActions {
setNotifications: (notifications: Notification[]) => void;
markRead: (id: string) => void;
markAllRead: () => void;
}Best Practices
- Batch notifications — Don't spam users with multiple similar notifications
- Include deep links — Make notifications actionable
- Truncate preview text — Keep body under 100 characters
- Clean up old notifications — Auto-delete after 30 days
Last updated: January 2026