mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
Add per-type notification preference controls to the Inbox, allowing users to toggle which notification types they receive. This addresses the core pain point of agent-generated status change noise flooding user inboxes. Backend: - New `notification_preference` table (migration 064) with per-user, per-workspace, per-type enabled/disabled toggle - GET/PUT /api/inbox/preferences endpoints for CRUD - notification_listeners.go checks user preferences before creating inbox items, using preloaded per-type batch queries for efficiency - status_changed notifications default to OFF; all others default ON Frontend: - NotificationPreference type and API client methods - TanStack Query options + optimistic mutation hook - Settings dialog accessible via gear icon in Inbox header - Grouped by category: Status, Comments, Assignments, Mentions, Priority, Due dates, Reactions, Agent events, Quick create - Each category shows icon, label, description, and Switch toggle
134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
)
|
|
|
|
// validNotificationTypes is the set of notification types that users can configure.
|
|
var validNotificationTypes = map[string]bool{
|
|
"issue_assigned": true,
|
|
"unassigned": true,
|
|
"assignee_changed": true,
|
|
"status_changed": true,
|
|
"priority_changed": true,
|
|
"due_date_changed": true,
|
|
"new_comment": true,
|
|
"mentioned": true,
|
|
"reaction_added": true,
|
|
"task_completed": true,
|
|
"task_failed": true,
|
|
"agent_blocked": true,
|
|
"agent_completed": true,
|
|
"quick_create_done": true,
|
|
"quick_create_failed": true,
|
|
}
|
|
|
|
// DefaultDisabledTypes are notification types that are OFF by default (no row = disabled).
|
|
// All other types default to ON (no row = enabled).
|
|
// Exported so notification_listeners can reference the same defaults.
|
|
var DefaultDisabledTypes = map[string]bool{
|
|
"status_changed": true,
|
|
}
|
|
|
|
type NotificationPreferenceResponse struct {
|
|
NotificationType string `json:"notification_type"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (h *Handler) ListNotificationPreferences(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := requireUserID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
workspaceID := ctxWorkspaceID(r.Context())
|
|
wsUUID, ok := parseUUIDOrBadRequest(w, workspaceID, "workspace id")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
prefs, err := h.Queries.ListNotificationPreferences(r.Context(), db.ListNotificationPreferencesParams{
|
|
WorkspaceID: wsUUID,
|
|
UserID: parseUUID(userID),
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list notification preferences")
|
|
return
|
|
}
|
|
|
|
// Build a map of explicitly set preferences
|
|
explicit := make(map[string]bool, len(prefs))
|
|
for _, p := range prefs {
|
|
explicit[p.NotificationType] = p.Enabled
|
|
}
|
|
|
|
// Build full response with defaults for all valid types
|
|
resp := make([]NotificationPreferenceResponse, 0, len(validNotificationTypes))
|
|
for t := range validNotificationTypes {
|
|
enabled := !DefaultDisabledTypes[t] // default: ON unless in defaultDisabledTypes
|
|
if v, ok := explicit[t]; ok {
|
|
enabled = v
|
|
}
|
|
resp = append(resp, NotificationPreferenceResponse{
|
|
NotificationType: t,
|
|
Enabled: enabled,
|
|
})
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
type updateNotificationPreferencesRequest struct {
|
|
Preferences []NotificationPreferenceResponse `json:"preferences"`
|
|
}
|
|
|
|
func (h *Handler) UpdateNotificationPreferences(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := requireUserID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
workspaceID := ctxWorkspaceID(r.Context())
|
|
wsUUID, ok := parseUUIDOrBadRequest(w, workspaceID, "workspace id")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req updateNotificationPreferencesRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if len(req.Preferences) == 0 {
|
|
writeError(w, http.StatusBadRequest, "preferences cannot be empty")
|
|
return
|
|
}
|
|
|
|
// Validate all notification types
|
|
for _, p := range req.Preferences {
|
|
if !validNotificationTypes[p.NotificationType] {
|
|
writeError(w, http.StatusBadRequest, "invalid notification type: "+p.NotificationType)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Upsert each preference
|
|
for _, p := range req.Preferences {
|
|
_, err := h.Queries.UpsertNotificationPreference(r.Context(), db.UpsertNotificationPreferenceParams{
|
|
WorkspaceID: wsUUID,
|
|
UserID: parseUUID(userID),
|
|
NotificationType: p.NotificationType,
|
|
Enabled: p.Enabled,
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update notification preference")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Return updated preferences
|
|
h.ListNotificationPreferences(w, r)
|
|
}
|