feat: add notification preference system with per-type per-channel toggles and admin defaults

Implement configurable notification preferences across in-app and push channels, including a new `notification_preferences` table with soft-delete support, per-user toggle endpoints, admin defaults management, and canonical type definitions. The change introduces `NOTIFICATION_TYPES` and `NOTIFICATION_CHANNELS` constants, `NotificationPrefForm`/`NotificationDefaultForm` models, `notification_enabled` resolution logic, and UI integration via the profile notifications tab and admin panel.
This commit is contained in:
2026-06-13 10:09:48 +00:00
parent 1a26428952
commit 5e4f0b1f3f
27 changed files with 1330 additions and 33 deletions
+21 -2
View File
@@ -562,8 +562,9 @@ installable Progressive Web App. Push uses only standard libraries (`cryptograph
### Events
Every event that already produces an in-app notification also sends a web push,
because both share a single funnel - `create_notification()` in `utils.py`:
Every event flows through a single funnel - `create_notification()` in `utils.py` -
which delivers on two independent channels, in-app and web push, each gated by the
recipient's preferences (see "Configurable notifications" below):
| Event | Recipient |
|-------|-----------|
@@ -573,6 +574,8 @@ because both share a single funnel - `create_notification()` in `utils.py`:
| `@mention` in any content | mentioned user |
| Upvote on your content | content owner |
| New follower | followed user |
| Badge earned / level-up | the user |
| Bug-tracker update | reporter / admins |
`create_notification` schedules delivery as a fire-and-forget async task, so a dead
subscription or push-service error never blocks the triggering request. Delivery
@@ -580,6 +583,22 @@ subscription or push-service error never blocks the triggering request. Delivery
(legacy `aesgcm` content encoding), and POSTs to each endpoint; subscriptions that
return `404`/`410` are soft-deleted.
### Configurable notifications
Every notification type can be turned on or off per channel, per user. The **Notifications**
tab on a profile page (`/profile/{username}?tab=notifications`, visible to the profile owner
and to admins) shows one row per type with two checkboxes - **In-app** and **Push** - saved
individually as you toggle them (`POST /profile/{username}/notifications`). A "Reset to
defaults" button clears all of a user's overrides (`POST /profile/{username}/notifications/reset`).
Defaults are opt-out: a type/channel a user never touched is enabled. Admins set the
platform-wide default for each type/channel on `/admin/notifications`
(`POST /admin/notifications`); a default applies only to users who have not made an explicit
choice. Resolution is: user override, else admin default, else on. Preferences are stored in
the `notification_preferences` table (per `user_uid` + `notification_type`, soft-deletable)
and enforced inside `create_notification()`: the in-app row is written only when the in-app
channel is enabled, and `push.notify_user` is scheduled only when the push channel is enabled.
### VAPID keys
The server identity is three PEM files generated once at startup in the repository