feat: chat reactions

This commit is contained in:
Krispire Admin
2026-08-30 01:51:32 +05:30
parent 9c458ba19f
commit cee2d3c874
8 changed files with 758 additions and 43 deletions
+74
View File
@@ -38,6 +38,8 @@ func writeChatError(w http.ResponseWriter, err error) {
response.Error(w, http.StatusNotFound, err.Error())
case errors.Is(err, service.ErrChatBadEntity):
response.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, service.ErrChatBadEmoji):
response.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, service.ErrChatBadQuote):
response.Error(w, http.StatusBadRequest, err.Error())
case errors.Is(err, service.ErrChatBadAvatarKey):
@@ -648,6 +650,78 @@ func (h *ChatHandler) SetMute(w http.ResponseWriter, r *http.Request) {
response.JSON(w, http.StatusOK, map[string]any{"muted_until": until})
}
// Message — GET /chat/conversations/{id}/messages/{messageID}
func (h *ChatHandler) Message(w http.ResponseWriter, r *http.Request) {
msg, err := h.svc.Message(r.Context(), middleware.UserID(r.Context()),
chi.URLParam(r, "id"), chi.URLParam(r, "messageID"))
if err != nil {
writeChatError(w, err)
return
}
response.JSON(w, http.StatusOK, msg)
}
// MessageReactions — GET /chat/conversations/{id}/messages/{messageID}/reactions
//
// Only read when someone opens the list; the thread itself renders from the
// grouped counts that ride along with each message.
func (h *ChatHandler) MessageReactions(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.MessageReactions(r.Context(),
middleware.UserID(r.Context()), chi.URLParam(r, "messageID"))
if err != nil {
writeChatError(w, err)
return
}
response.JSON(w, http.StatusOK, list)
}
// Typing — POST /chat/conversations/{id}/typing
//
// Fire-and-forget: nothing is stored and the response carries nothing, because
// a typing indicator is stale within seconds of being sent.
func (h *ChatHandler) Typing(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Typing(r.Context(),
middleware.UserID(r.Context()), chi.URLParam(r, "id")); err != nil {
writeChatError(w, err)
return
}
response.NoContent(w)
}
type reactReq struct {
Emoji string `json:"emoji"`
}
// React — PUT /chat/conversations/{id}/messages/{messageID}/reaction.
// Replaces any previous choice.
func (h *ChatHandler) React(w http.ResponseWriter, r *http.Request) {
var req reactReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid JSON body")
return
}
// The message id is authoritative: React re-derives the conversation from
// it and authorizes there, so the id in the path can't be used to react
// into a thread the caller isn't in.
if err := h.svc.React(r.Context(), middleware.UserID(r.Context()),
chi.URLParam(r, "messageID"), req.Emoji); err != nil {
writeChatError(w, err)
return
}
response.NoContent(w)
}
// Unreact — DELETE /chat/conversations/{id}/messages/{messageID}/reaction.
// Idempotent.
func (h *ChatHandler) Unreact(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Unreact(r.Context(), middleware.UserID(r.Context()),
chi.URLParam(r, "messageID")); err != nil {
writeChatError(w, err)
return
}
response.NoContent(w)
}
// Receipts — GET /chat/conversations/{id}/receipts
func (h *ChatHandler) Receipts(w http.ResponseWriter, r *http.Request) {
rc, err := h.svc.Receipts(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id"))