From 0091f10072ac5fc56f4855907eb0e211430b4a71 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Mon, 12 Jan 2026 07:25:44 +0000 Subject: [PATCH] appview: fetch `aturi->reactionMap` from DB There can be multiple reactable entities in same page. Fetch every reactions in `aturi->reactionMap` format where reactionMap is `kind->T` map Signed-off-by: Seongmin Lee --- appview/db/reaction.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------- appview/issues/issues.go | 14 +++++++++----- appview/pages/pages.go | 8 ++++---- appview/pages/templates/repo/issues/issue.html | 7 ++++--- appview/pages/templates/repo/pulls/fragments/pullHeader.html | 7 ++++--- appview/pulls/single.go | 12 ++++++++---- 6 file(s) changed, 128 insertion(s)(+), 54 deletion(s)(-) diff --git a/appview/db/reaction.go b/appview/db/reaction.go --- a/appview/db/reaction.go +++ b/appview/db/reaction.go @@ -1,11 +1,13 @@ package db import ( + "fmt" "log" "time" "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/models" + "tangled.org/core/orm" ) func AddReaction(e Execer, reactedByDid string, threadAt syntax.ATURI, kind models.ReactionKind, rkey string) error { @@ -71,58 +73,120 @@ } return count, nil } +// GetReactionDisplayDataMap returns map of [models.ReactionKind]->[models.ReactionDisplayData] func GetReactionMap(e Execer, userLimit int, threadAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { - query := ` - select kind, reacted_by_did, - row_number() over (partition by kind order by created asc) as rn, - count(*) over (partition by kind) as total - from reactions - where thread_at = ? - order by kind, created asc` + reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{threadAt}, userLimit) + return reactionMaps[threadAt], err +} + +// ListReactionDisplayDataMap returns map of [syntax.ATURI]->[models.ReactionKind]->[models.ReactionDisplayData] +func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) (map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData, error) { + if len(threads) == 0 { + return nil, nil + } - rows, err := e.Query(query, threadAt) + filter := orm.FilterIn("thread_at", threads) + args := filter.Arg() + args = append(args, userLimit) + rows, err := e.Query( + fmt.Sprintf( + `with ranked_reactions as ( + select + thread_at, + kind, + reacted_by_did, + row_number() over (partition by thread_at, kind order by created asc) as rn, + count(*) over (partition by thread_at, kind) as total + from reactions + where %s + ) + select thread_at, kind, reacted_by_did, total + from ranked_reactions + where rn <= ? + order by thread_at, kind, rn asc`, + filter.Condition(), + ), + args..., + ) if err != nil { - return nil, err + return nil, fmt.Errorf("querying: %w", err) } defer rows.Close() - reactionMap := map[models.ReactionKind]models.ReactionDisplayData{} - for _, kind := range models.OrderedReactionKinds { - reactionMap[kind] = models.ReactionDisplayData{Count: 0, Users: []string{}} - } + // aturi -> kind -> {count,users} + result := make(map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData) for rows.Next() { + var aturi syntax.ATURI var kind models.ReactionKind - var did string - var rn, total int - if err := rows.Scan(&kind, &did, &rn, &total); err != nil { - return nil, err + var did syntax.DID + var count int + + if err := rows.Scan(&aturi, &kind, &did, &count); err != nil { + return nil, fmt.Errorf("scanning row: %w", err) } - data := reactionMap[kind] - data.Count = total - if userLimit > 0 && rn <= userLimit { - data.Users = append(data.Users, did) + if _, ok := result[aturi]; !ok { + result[aturi] = make(map[models.ReactionKind]models.ReactionDisplayData) } - reactionMap[kind] = data + data := result[aturi][kind] + data.Count = count + data.Users = append(data.Users, did.String()) + result[aturi][kind] = data + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate rows: %w", err) } - return reactionMap, rows.Err() + return result, nil +} + +// GetReactionStatusMap returns map of [models.ReactionKind]->[bool] +func GetReactionStatusMap(e Execer, userDid syntax.DID, threadAt syntax.ATURI) (map[models.ReactionKind]bool, error) { + reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{threadAt}, userDid) + return reactionMaps[threadAt], err } -func GetReactionStatus(e Execer, userDid string, threadAt syntax.ATURI, kind models.ReactionKind) bool { - if _, err := GetReaction(e, userDid, threadAt, kind); err != nil { - return false - } else { - return true +// ListReactionStatusMap returns map of [syntax.ATURI]->[models.ReactionKind]->[bool] +func ListReactionStatusMap(e Execer, threads []syntax.ATURI, userDid syntax.DID) (map[syntax.ATURI]map[models.ReactionKind]bool, error) { + if len(threads) == 0 { + return nil, nil + } + + filter := orm.FilterIn("thread_at", threads) + args := []any{userDid} + args = append(args, filter.Arg()...) + rows, err := e.Query( + fmt.Sprintf( + `select thread_at, kind from reactions + where reacted_by_did = ? and %s`, + filter.Condition(), + ), + args..., + ) + if err != nil { + return nil, err } -} + defer rows.Close() + + // aturi -> kind -> bool + result := make(map[syntax.ATURI]map[models.ReactionKind]bool) + + for rows.Next() { + var aturi syntax.ATURI + var kind models.ReactionKind + + if err := rows.Scan(&aturi, &kind); err != nil { + return nil, fmt.Errorf("scanning row: %w", err) + } -func GetReactionStatusMap(e Execer, userDid string, threadAt syntax.ATURI) map[models.ReactionKind]bool { - statusMap := map[models.ReactionKind]bool{} - for _, kind := range models.OrderedReactionKinds { - count := GetReactionStatus(e, userDid, threadAt, kind) - statusMap[kind] = count + if _, ok := result[aturi]; !ok { + result[aturi] = make(map[models.ReactionKind]bool) + } + + result[aturi][kind] = true } - return statusMap + + return result, nil } diff --git a/appview/issues/issues.go b/appview/issues/issues.go --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -98,14 +98,18 @@ rp.pages.Error404(w) return } - reactionMap, err := db.GetReactionMap(rp.db, 20, issue.AtUri()) + entities := []syntax.ATURI{issue.AtUri()} + reactions, err := db.ListReactionDisplayDataMap(rp.db, entities, 20) if err != nil { - l.Error("failed to get issue reactions", "err", err) + l.Error("failed to get reactions", "err", err) } - userReactions := map[models.ReactionKind]bool{} + var userReactions map[syntax.ATURI]map[models.ReactionKind]bool if user != nil { - userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) + userReactions, err = db.ListReactionStatusMap(rp.db, entities, syntax.DID(user.Did)) + if err != nil { + l.Error("failed to get user reactions", "err", err) + } } backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) @@ -146,7 +150,7 @@ RepoInfo: rp.repoResolver.GetRepoInfo(r, user), Issue: issue, CommentList: models.NewCommentList(issue.Comments), Backlinks: backlinks, - Reactions: reactionMap, + Reactions: reactions, UserReacted: userReactions, LabelDefs: defs, VouchRelationships: vouchRelationships, diff --git a/appview/pages/pages.go b/appview/pages/pages.go --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -1188,8 +1188,8 @@ CommentList []models.CommentListItem Backlinks []models.RichReferenceLink LabelDefs map[string]*models.LabelDefinition - Reactions map[models.ReactionKind]models.ReactionDisplayData - UserReacted map[models.ReactionKind]bool + Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData + UserReacted map[syntax.ATURI]map[models.ReactionKind]bool VouchRelationships map[syntax.DID]*models.VouchRelationship } @@ -1337,8 +1337,8 @@ DiffOpts types.DiffOpts ActiveRound int IsInterdiff bool - Reactions map[models.ReactionKind]models.ReactionDisplayData - UserReacted map[models.ReactionKind]bool + Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData + UserReacted map[syntax.ATURI]map[models.ReactionKind]bool LabelDefs map[string]*models.LabelDefinition VouchRelationships map[syntax.DID]*models.VouchRelationship diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -36,10 +36,11 @@ {{ if .Issue.Body }}
{{ .Issue.Body | markdown }}
{{ end }}
+ {{ $aturi := .Issue.AtUri }} {{ template "repo/fragments/reactions" - (dict "Reactions" .Reactions - "UserReacted" .UserReacted - "ThreadAt" .Issue.AtUri) }} + (dict "Reactions" (index .Reactions $aturi) + "UserReacted" (index .UserReacted $aturi) + "ThreadAt" $aturi) }}
{{ end }} diff --git a/appview/pages/templates/repo/pulls/fragments/pullHeader.html b/appview/pages/templates/repo/pulls/fragments/pullHeader.html --- a/appview/pages/templates/repo/pulls/fragments/pullHeader.html +++ b/appview/pages/templates/repo/pulls/fragments/pullHeader.html @@ -49,10 +49,11 @@ {{ end }}
+ {{ $aturi := .Pull.AtUri }} {{ template "repo/fragments/reactions" - (dict "Reactions" .Reactions - "UserReacted" .UserReacted - "ThreadAt" .Pull.AtUri) }} + (dict "Reactions" (index .Reactions $aturi) + "UserReacted" (index .UserReacted $aturi) + "ThreadAt" $aturi) }}
diff --git a/appview/pulls/single.go b/appview/pulls/single.go --- a/appview/pulls/single.go +++ b/appview/pulls/single.go @@ -162,14 +162,18 @@ for _, p := range ps { m[p.Sha] = p } - reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) + entities := []syntax.ATURI{pull.AtUri()} + reactions, err := db.ListReactionDisplayDataMap(s.db, entities, 20) if err != nil { l.Error("failed to get pull reactions", "err", err) } - userReactions := map[models.ReactionKind]bool{} + var userReactions map[syntax.ATURI]map[models.ReactionKind]bool if user != nil { - userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri()) + userReactions, err = db.ListReactionStatusMap(s.db, entities, syntax.DID(user.Did)) + if err != nil { + s.logger.Error("failed to get user reactions", "err", err) + } } labelDefs, err := db.GetLabelDefinitions( @@ -241,7 +245,7 @@ DiffOpts: diffOpts, ActiveRound: roundIdInt, IsInterdiff: interdiff, - Reactions: reactionMap, + Reactions: reactions, UserReacted: userReactions, LabelDefs: defs, -- tangled.sh