feat: granular per-category push notification settings

New Settings → Notifications section with a toggle per category (follow,
comment, reply, reaction, rating, mention, leftover-expiring, shared
shopping list) — previously it was all-or-nothing (browser permission only).

userNotificationPrefs (one row per user, defaults all-on so existing users
see no behavior change until they opt out of something). Gated the push
send in the three places that dispatch one: lib/notifications.ts (the 6
in-app notification types), the leftover-expiry cron, and shopping-list-notify
— in-app notification-center entries and email are unaffected, this only
gates the push itself, matching the literal ask.

Verified locally: GET/PUT round-trips correctly, disabling a category
and confirming the settings page renders all 8 toggles.
This commit is contained in:
Arnaud
2026-07-12 19:07:32 +02:00
parent e78c959c49
commit 4d5269aced
13 changed files with 5037 additions and 6 deletions
+18
View File
@@ -145,10 +145,28 @@ export const userNutritionGoals = pgTable("user_nutrition_goals", {
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const userNotificationPrefs = pgTable("user_notification_prefs", {
id: text("id").primaryKey(),
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }).unique(),
follow: boolean("follow").notNull().default(true),
comment: boolean("comment").notNull().default(true),
reply: boolean("reply").notNull().default(true),
reaction: boolean("reaction").notNull().default(true),
rating: boolean("rating").notNull().default(true),
mention: boolean("mention").notNull().default(true),
leftoverExpiring: boolean("leftover_expiring").notNull().default(true),
shoppingList: boolean("shopping_list").notNull().default(true),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const pushSubscriptionsRelations = relations(pushSubscriptions, ({ one }) => ({
user: one(users, { fields: [pushSubscriptions.userId], references: [users.id] }),
}));
export const userNotificationPrefsRelations = relations(userNotificationPrefs, ({ one }) => ({
user: one(users, { fields: [userNotificationPrefs.userId], references: [users.id] }),
}));
export const userNutritionGoalsRelations = relations(userNutritionGoals, ({ one }) => ({
user: one(users, { fields: [userNutritionGoals.userId], references: [users.id] }),
}));