package readstate 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}/read", h.MarkRead) r.Get("/users/me/read-states", h.GetAll) } type markReadRequest struct { LastReadMessageID string `json:"last_read_message_id"` } func (h *Handler) MarkRead(w http.ResponseWriter, r *http.Request) { userID, _ := middleware.UserIDFromContext(r.Context()) channelID := chi.URLParam(r, "channelID") var req markReadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest) return } if req.LastReadMessageID == "" { http.Error(w, `{"error":"last_read_message_id is required"}`, http.StatusBadRequest) return } _, err := h.db.Exec(` INSERT INTO read_states (user_id, channel_id, last_read_message_id, updated_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (user_id, channel_id) DO UPDATE SET last_read_message_id = $3, updated_at = NOW() `, userID, channelID, req.LastReadMessageID) if err != nil { h.logger.Error("failed to mark read", "error", err) 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"}) } // ChannelReadState is returned per channel type ChannelReadState struct { ChannelID string `json:"channel_id"` LastReadMessageID string `json:"last_read_message_id"` } func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) { userID, _ := middleware.UserIDFromContext(r.Context()) rows, err := h.db.Query(` SELECT rs.channel_id, rs.last_read_message_id FROM read_states rs WHERE rs.user_id = $1 `, userID) if err != nil { h.logger.Error("failed to query read states", "error", err) http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) return } defer rows.Close() states := make(map[string]string) for rows.Next() { var channelID, lastReadMessageID string if err := rows.Scan(&channelID, &lastReadMessageID); err != nil { continue } states[channelID] = lastReadMessageID } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(states) }