internal/store/reactions.go

v1.43.1
gitbay/internal/store/reactions.go history · blame · raw

148 lines · 4251 bytes

  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}