feat(reactions): add emoji reactions for comments and issue descriptions

Add Slack-style emoji reactions to comments and issue descriptions with
full-stack support: database tables, REST API endpoints, real-time
WebSocket sync, optimistic UI updates, and inbox notifications.

- New `comment_reaction` and `issue_reaction` tables with migrations
- POST/DELETE endpoints for adding/removing reactions on both comments
  and issue descriptions
- Real-time WS events (reaction:added/removed, issue_reaction:added/removed)
- Shared ReactionBar component with quick emoji picker and full emoji-mart
  picker (lazy-loaded)
- Optimistic add/remove with rollback on failure
- Inbox notifications for comment author and issue creator when reacted to
- Reactions included in timeline, comment list, and issue detail responses
This commit is contained in:
Jiayuan 2026-03-30 22:37:59 +08:00
parent 72e3ccfe33
commit 7c1aabbe3a
32 changed files with 1221 additions and 49 deletions

View file

@ -18,23 +18,24 @@ import (
// IssueResponse is the JSON response for an issue.
type IssueResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Number int32 `json:"number"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
CreatorType string `json:"creator_type"`
CreatorID string `json:"creator_id"`
ParentIssueID *string `json:"parent_issue_id"`
Position float64 `json:"position"`
DueDate *string `json:"due_date"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Number int32 `json:"number"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
CreatorType string `json:"creator_type"`
CreatorID string `json:"creator_id"`
ParentIssueID *string `json:"parent_issue_id"`
Position float64 `json:"position"`
DueDate *string `json:"due_date"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Reactions []IssueReactionResponse `json:"reactions,omitempty"`
}
type agentTriggerSnapshot struct {
@ -130,7 +131,18 @@ func (h *Handler) GetIssue(w http.ResponseWriter, r *http.Request) {
return
}
prefix := h.getIssuePrefix(r.Context(), issue.WorkspaceID)
writeJSON(w, http.StatusOK, issueToResponse(issue, prefix))
resp := issueToResponse(issue, prefix)
// Fetch issue reactions.
reactions, err := h.Queries.ListIssueReactions(r.Context(), issue.ID)
if err == nil && len(reactions) > 0 {
resp.Reactions = make([]IssueReactionResponse, len(reactions))
for i, rx := range reactions {
resp.Reactions[i] = issueReactionToResponse(rx)
}
}
writeJSON(w, http.StatusOK, resp)
}
type CreateIssueRequest struct {