feat(notifications): inbox query + mark-read mutations
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { v } from 'convex/values';
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import type { MutationCtx } from './_generated/server';
|
||||
import { internal } from './_generated/api';
|
||||
import { internalMutation, internalQuery } from './_generated/server';
|
||||
import {
|
||||
internalMutation,
|
||||
internalQuery,
|
||||
mutation,
|
||||
query,
|
||||
} from './_generated/server';
|
||||
import { getRequiredUserId } from './model';
|
||||
|
||||
export type NotificationKind =
|
||||
| 'maintenance_thread'
|
||||
@@ -139,3 +145,78 @@ export const getForEmail = internalQuery({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The authed caller's notifications, newest-first. Scoped to the caller via
|
||||
* `getRequiredUserId`; never accepts a userId argument.
|
||||
*/
|
||||
export const listMine = query({
|
||||
args: { limit: v.optional(v.number()) },
|
||||
handler: async (ctx, args): Promise<Doc<'notifications'>[]> => {
|
||||
const userId = await getRequiredUserId(ctx);
|
||||
return await ctx.db
|
||||
.query('notifications')
|
||||
.withIndex('by_user_created', (q) => q.eq('userId', userId))
|
||||
.order('desc')
|
||||
.take(args.limit ?? 30);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* How many of the caller's notifications are unread. Bounded at 100 via the
|
||||
* `by_user_unread` index so the header bell stays cheap.
|
||||
*/
|
||||
export const unreadCount = query({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<number> => {
|
||||
const userId = await getRequiredUserId(ctx);
|
||||
const unread = await ctx.db
|
||||
.query('notifications')
|
||||
.withIndex('by_user_unread', (q) =>
|
||||
q.eq('userId', userId).eq('readAt', undefined),
|
||||
)
|
||||
.take(100);
|
||||
return unread.length;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Marks a single notification read. Loads the row and asserts it belongs to the
|
||||
* caller before patching, so a user can never mark another user's notification
|
||||
* read (indistinguishable from a missing row).
|
||||
*/
|
||||
export const markRead = mutation({
|
||||
args: { notificationId: v.id('notifications') },
|
||||
handler: async (ctx, { notificationId }): Promise<void> => {
|
||||
const userId = await getRequiredUserId(ctx);
|
||||
const notification = await ctx.db.get(notificationId);
|
||||
if (notification?.userId !== userId) {
|
||||
throw new ConvexError('Notification not found.');
|
||||
}
|
||||
if (notification.readAt === undefined) {
|
||||
await ctx.db.patch(notificationId, { readAt: Date.now() });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Marks all of the caller's unread notifications read. Iterates the caller's
|
||||
* unread rows via the `by_user_unread` index; only ever touches the caller's
|
||||
* own notifications.
|
||||
*/
|
||||
export const markAllRead = mutation({
|
||||
args: {},
|
||||
handler: async (ctx): Promise<void> => {
|
||||
const userId = await getRequiredUserId(ctx);
|
||||
const unread = await ctx.db
|
||||
.query('notifications')
|
||||
.withIndex('by_user_unread', (q) =>
|
||||
q.eq('userId', userId).eq('readAt', undefined),
|
||||
)
|
||||
.collect();
|
||||
const now = Date.now();
|
||||
for (const notification of unread) {
|
||||
await ctx.db.patch(notification._id, { readAt: now });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user