internal/store/reactions.go
148 lines · 4251 bytes
9 symbols in this file
1package store
2
3import "fmt"
4
5// Reaction is one entry of the fixed reaction set (#291).
6type Reaction struct {
7 Name string // what is stored and what the commands take
8 Emoji string // what surfaces render
9}
10
11// Reactions is the whole set, in display order.
12var Reactions = []Reaction{
13 {"+1", "👍"}, {"-1", "👎"}, {"laugh", "😄"}, {"hooray", "🎉"},
14 {"confused", "😕"}, {"heart", "❤️"}, {"rocket", "🚀"}, {"eyes", "👀"},
15}
16
17// ParseReaction maps a name or its emoji to the stored name.
18func ParseReaction(v string) (string, bool) {
19 for _, r := range Reactions {
20 if v == r.Name || v == r.Emoji {
21 return r.Name, true
22 }
23 }
24 if v == "❤" { // heart without the emoji variation selector
25 return "heart", true
26 }
27 return "", false
28}
29
30// ReactionEmoji is the emoji for a stored name.
31func ReactionEmoji(name string) string {
32 for _, r := range Reactions {
33 if r.Name == name {
34 return r.Emoji
35 }
36 }
37 return name
38}
39
40// ReactionCount is how many people gave one reaction to one item, and
41// whether the viewer is among them.
42type ReactionCount struct {
43 Reaction string
44 Count int
45 Me bool
46}
47
48// reactionTables names the tables for a noun, "issue" or "mr".
49func reactionTables(noun string) (reactions, threadCol, comments string, err error) {
50 switch noun {
51 case "issue":
52 return "issue_reactions", "issue_id", "issue_comments", nil
53 case "mr":
54 return "mr_reactions", "mr_id", "mr_comments", nil
55 }
56 return "", "", "", fmt.Errorf("no reactions on %q", noun)
57}
58
59// ReactionTarget checks that commentID is a conversation comment of the
60// thread and not a system entry. commentID 0 is the thread itself.
61func (s *Store) ReactionTarget(noun string, threadID, commentID int64) error {
62 if commentID == 0 {
63 return nil
64 }
65 _, threadCol, comments, err := reactionTables(noun)
66 if err != nil {
67 return err
68 }
69 var n int
70 if err := s.DB.QueryRow("SELECT COUNT(*) FROM "+comments+
71 " WHERE id = ? AND "+threadCol+" = ? AND kind != 'system'",
72 commentID, threadID).Scan(&n); err != nil {
73 return err
74 }
75 if n == 0 {
76 return ErrNotFound
77 }
78 return nil
79}
80
81// SetReaction adds or removes one user's reaction. Doing either twice is
82// not an error. commentID 0 targets the thread's own body.
83func (s *Store) SetReaction(noun string, threadID, commentID, userID int64, reaction string, on bool) error {
84 table, threadCol, _, err := reactionTables(noun)
85 if err != nil {
86 return err
87 }
88 var cid any
89 cond := "comment_id IS NULL"
90 if commentID != 0 {
91 cid, cond = commentID, "comment_id = ?"
92 }
93 if !on {
94 args := []any{threadID, userID, reaction}
95 if commentID != 0 {
96 args = append(args, cid)
97 }
98 _, err := s.DB.Exec("DELETE FROM "+table+" WHERE "+threadCol+" = ? AND user_id = ? AND reaction = ? AND "+cond, args...)
99 return err
100 }
101 _, err = s.DB.Exec("INSERT INTO "+table+" ("+threadCol+", comment_id, user_id, reaction) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING",
102 threadID, cid, userID, reaction)
103 return err
104}
105
106// ReactionCounts returns the counts for a thread and its comments, keyed
107// by comment id, with 0 for the thread itself. Each list is in the
108// order of Reactions and holds only reactions somebody gave. viewerID 0
109// marks none as the viewer's.
110func (s *Store) ReactionCounts(noun string, threadID, viewerID int64) (map[int64][]ReactionCount, error) {
111 table, threadCol, _, err := reactionTables(noun)
112 if err != nil {
113 return nil, err
114 }
115 rows, err := s.DB.Query(`SELECT COALESCE(comment_id, 0), reaction, COUNT(*),
116 COALESCE(MAX(user_id = ?), 0) FROM `+table+` WHERE `+threadCol+` = ?
117 GROUP BY comment_id, reaction`, viewerID, threadID)
118 if err != nil {
119 return nil, err
120 }
121 defer rows.Close()
122 by := map[int64]map[string]ReactionCount{}
123 for rows.Next() {
124 var cid int64
125 var rc ReactionCount
126 var me int
127 if err := rows.Scan(&cid, &rc.Reaction, &rc.Count, &me); err != nil {
128 return nil, err
129 }
130 rc.Me = me != 0 && viewerID != 0
131 if by[cid] == nil {
132 by[cid] = map[string]ReactionCount{}
133 }
134 by[cid][rc.Reaction] = rc
135 }
136 if err := rows.Err(); err != nil {
137 return nil, err
138 }
139 out := map[int64][]ReactionCount{}
140 for cid, m := range by {
141 for _, r := range Reactions {
142 if rc, ok := m[r.Name]; ok {
143 out[cid] = append(out[cid], rc)
144 }
145 }
146 }
147 return out, nil
148}