cc6ed741f0
Phase 7.1 — Per-channel notification settings
- New notification_settings table (user_id, channel_id, level)
- GET/PUT/DELETE handlers under /channels/{channelID}/notifications
- Push dispatch and @mention loops filtered by notification level
- Frontend: bell icon per channel cycling all/mentions/none
Phase 7.4 — Read receipts
- New read_states table (user_id, channel_id, last_read_message_id)
- PUT /channels/{channelID}/read and GET /users/me/read-states
- Auto-mark-read when messages load in ChatArea
- Unread dot for channels never opened
Also: favicon/icon refresh in index.html
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
package notification
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *sql.DB
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewHandler(db *sql.DB, logger *slog.Logger) *Handler {
|
|
return &Handler{db: db, logger: logger}
|
|
}
|
|
|
|
func (h *Handler) RegisterRoutes(r chi.Router) {
|
|
r.Put("/{channelID}/notifications", h.Set)
|
|
r.Get("/me/notifications", h.GetAll)
|
|
}
|
|
|
|
type setNotificationRequest struct {
|
|
Level string `json:"level"`
|
|
}
|
|
|
|
func (h *Handler) Set(w http.ResponseWriter, r *http.Request) {
|
|
userID, _ := middleware.UserIDFromContext(r.Context())
|
|
channelID := chi.URLParam(r, "channelID")
|
|
|
|
var req setNotificationRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Level != "all" && req.Level != "mentions" && req.Level != "none" {
|
|
http.Error(w, `{"error":"level must be all, mentions, or none"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, err := h.db.Exec(`
|
|
INSERT INTO notification_settings (user_id, channel_id, level)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (user_id, channel_id) DO UPDATE SET level = $3
|
|
`, userID, channelID, req.Level)
|
|
if err != nil {
|
|
h.logger.Error("failed to set notification level", "error", err, "user_id", userID, "channel_id", channelID)
|
|
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "level": req.Level})
|
|
}
|
|
|
|
type ChannelSetting struct {
|
|
ChannelID string `json:"channel_id"`
|
|
Level string `json:"level"`
|
|
}
|
|
|
|
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
|
|
userID, _ := middleware.UserIDFromContext(r.Context())
|
|
|
|
rows, err := h.db.Query(`
|
|
SELECT channel_id, level FROM notification_settings WHERE user_id = $1
|
|
`, userID)
|
|
if err != nil {
|
|
h.logger.Error("failed to query notification settings", "error", err)
|
|
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
settings := make(map[string]string)
|
|
for rows.Next() {
|
|
var channelID, level string
|
|
if err := rows.Scan(&channelID, &level); err != nil {
|
|
continue
|
|
}
|
|
settings[channelID] = level
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(settings)
|
|
}
|