diff --git a/appview/db/vouch.go b/appview/db/vouch.go index 37ede8f2..b29645d1 100644 --- a/appview/db/vouch.go +++ b/appview/db/vouch.go @@ -64,38 +64,11 @@ func GetVouches(e Execer, limit int, filters ...orm.Filter) ([]models.Vouch, err defer rows.Close() for rows.Next() { - var vouch models.Vouch - var createdAt string - var cidStr string - var reason sql.NullString - err := rows.Scan( - &vouch.Did, - &vouch.SubjectDid, - &cidStr, - &vouch.Kind, - &reason, - &createdAt, - ) + vouch, err := scanVouch(rows) if err != nil { - return nil, err - } - - vouch.Cid, err = cid.Parse(cidStr) - if err != nil { - log.Println("unable to parse CID:", err) + log.Println("error scanning vouch:", err) continue } - - createdAtTime, err := time.Parse(time.RFC3339, createdAt) - if err != nil { - log.Println("unable to determine created at time") - vouch.CreatedAt = time.Now() - } else { - vouch.CreatedAt = createdAtTime - } - if reason.Valid { - vouch.Reason = &reason.String - } vouches = append(vouches, vouch) } return vouches, nil @@ -199,64 +172,173 @@ func GetNetworkVouchesForSubject(e Execer, viewerDid, subjectDid string, limit i var vouches []models.Vouch for rows.Next() { - var vouch models.Vouch - var createdAt string - var cidStr string - var reason sql.NullString - err := rows.Scan( - &vouch.Did, - &vouch.SubjectDid, - &cidStr, - &vouch.Kind, - &reason, - &createdAt, - ) + vouch, err := scanVouch(rows) if err != nil { - return nil, err - } - - vouch.Cid, err = cid.Parse(cidStr) - if err != nil { - log.Println("unable to parse CID:", err) + log.Println("error scanning vouch:", err) continue } - - createdAtTime, err := time.Parse(time.RFC3339, createdAt) - if err != nil { - log.Println("unable to determine created at time") - vouch.CreatedAt = time.Now() - } else { - vouch.CreatedAt = createdAtTime - } - if reason.Valid { - vouch.Reason = &reason.String - } vouches = append(vouches, vouch) } return vouches, nil } -func GetVouchRelationship(e Execer, viewerDid, subjectDid string) (*models.VouchRelationship, error) { - relationship := &models.VouchRelationship{ - ViewerDid: viewerDid, - SubjectDid: subjectDid, - NetworkVouches: []models.Vouch{}, +// scanVouch scans a single vouch row from the database +func scanVouch(rows *sql.Rows) (models.Vouch, error) { + var vouch models.Vouch + var createdAt string + var cidStr string + var reason sql.NullString + err := rows.Scan( + &vouch.Did, + &vouch.SubjectDid, + &cidStr, + &vouch.Kind, + &reason, + &createdAt, + ) + if err != nil { + return models.Vouch{}, err + } + + vouch.Cid, err = cid.Parse(cidStr) + if err != nil { + log.Println("unable to parse CID:", err) + return models.Vouch{}, err + } + + createdAtTime, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + log.Println("unable to determine created at time") + vouch.CreatedAt = time.Now() + } else { + vouch.CreatedAt = createdAtTime } + if reason.Valid { + vouch.Reason = &reason.String + } + return vouch, nil +} + +// GetVouchRelationshipsBatch retrieves vouch relationships for multiple subjects from the viewer's perspective +func GetVouchRelationshipsBatch(e Execer, viewerDid string, subjectDids []string) (map[string]*models.VouchRelationship, error) { + if viewerDid == "" { + return nil, fmt.Errorf("viewerDid cannot be empty") + } + + // Initialize result map with empty relationships for all subjects + result := make(map[string]*models.VouchRelationship) + for _, subjectDid := range subjectDids { + result[subjectDid] = &models.VouchRelationship{ + ViewerDid: viewerDid, + SubjectDid: subjectDid, + NetworkVouches: []models.Vouch{}, + } + } + + if len(subjectDids) == 0 { + return result, nil + } + + // Build placeholders for IN clause + placeholders := make([]string, len(subjectDids)) + args := make([]any, len(subjectDids)+1) + args[0] = viewerDid + for i, did := range subjectDids { + placeholders[i] = "?" + args[i+1] = did + } + placeholderStr := strings.Join(placeholders, ",") + + // Query 1: Get direct vouches from viewer to all subjects + directQuery := fmt.Sprintf(` + SELECT did, subject_did, cid, kind, reason, created_at + FROM vouches + WHERE did = ? AND subject_did IN (%s) + `, placeholderStr) - // Get direct vouch from viewer to subject (if exists) - directVouch, err := GetVouch(e, viewerDid, subjectDid) - if err == nil { - relationship.NetworkVouches = append(relationship.NetworkVouches, *directVouch) - } else if err != sql.ErrNoRows { + rows, err := e.Query(directQuery, args...) + if err != nil { return nil, err } + defer rows.Close() + + for rows.Next() { + vouch, err := scanVouch(rows) + if err != nil { + return nil, err + } + if rel, ok := result[vouch.SubjectDid]; ok { + rel.NetworkVouches = append(rel.NetworkVouches, vouch) + } + } - networkVouches, err := GetNetworkVouchesForSubject(e, viewerDid, subjectDid, 0) + // Query 2a: Get viewer's network (people viewer has vouched for) + networkQuery := ` + SELECT subject_did + FROM vouches + WHERE did = ? AND kind = 'vouch' + ` + rows, err = e.Query(networkQuery, viewerDid) if err != nil { return nil, err } + defer rows.Close() + + var network []string + for rows.Next() { + var did string + if err := rows.Scan(&did); err != nil { + return nil, err + } + network = append(network, did) + } + + // Query 2b: If viewer has a network, get vouches from network to subjects + if len(network) > 0 { + // Build placeholders for network DIDs + networkPlaceholders := make([]string, len(network)) + networkArgs := make([]any, 0, len(subjectDids)+len(network)) + + // First add subject DIDs + for _, did := range subjectDids { + networkArgs = append(networkArgs, did) + } + // Then add network DIDs + for i, did := range network { + networkPlaceholders[i] = "?" + networkArgs = append(networkArgs, did) + } + + networkVouchQuery := fmt.Sprintf(` + SELECT did, subject_did, cid, kind, reason, created_at + FROM vouches + WHERE subject_did IN (%s) AND did IN (%s) + `, placeholderStr, strings.Join(networkPlaceholders, ",")) + + rows, err = e.Query(networkVouchQuery, networkArgs...) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + vouch, err := scanVouch(rows) + if err != nil { + return nil, err + } + if rel, ok := result[vouch.SubjectDid]; ok { + rel.NetworkVouches = append(rel.NetworkVouches, vouch) + } + } + } - relationship.NetworkVouches = append(relationship.NetworkVouches, networkVouches...) + return result, nil +} - return relationship, nil +func GetVouchRelationship(e Execer, viewerDid, subjectDid string) (*models.VouchRelationship, error) { + batch, err := GetVouchRelationshipsBatch(e, viewerDid, []string{subjectDid}) + if err != nil { + return nil, err + } + return batch[subjectDid], nil } diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 9305aab5..4f74baf0 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -127,9 +127,13 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { return } - var vouchRelationship *models.VouchRelationship + vouchRelationships := make(map[string]*models.VouchRelationship) if user != nil { - vouchRelationship, err := db.GetVouchRelationship(rp.db, user.Did, issue.Did) + participants := issue.Participants() + vouchRelationships, err = db.GetVouchRelationshipsBatch(rp.db, user.Did, participants) + if err != nil { + l.Error("failed to fetch vouch relationships", "err", err) + } } defs := make(map[string]*models.LabelDefinition) @@ -138,14 +142,15 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { } rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ - LoggedInUser: user, - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), - Issue: issue, - CommentList: issue.CommentList(), - Backlinks: backlinks, - Reactions: reactionMap, - UserReacted: userReactions, - LabelDefs: defs, + LoggedInUser: user, + RepoInfo: rp.repoResolver.GetRepoInfo(r, user), + Issue: issue, + CommentList: issue.CommentList(), + Backlinks: backlinks, + Reactions: reactionMap, + UserReacted: userReactions, + LabelDefs: defs, + VouchRelationships: vouchRelationships, }) } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 39c7ce37..2bf40314 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -1131,8 +1131,9 @@ type RepoSingleIssueParams struct { Backlinks []models.RichReferenceLink LabelDefs map[string]*models.LabelDefinition - Reactions map[models.ReactionKind]models.ReactionDisplayData - UserReacted map[models.ReactionKind]bool + Reactions map[models.ReactionKind]models.ReactionDisplayData + UserReacted map[models.ReactionKind]bool + VouchRelationships map[string]*models.VouchRelationship } func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { diff --git a/appview/pages/templates/fragments/icons/shield-alert.html b/appview/pages/templates/fragments/icons/shield-alert.html new file mode 100644 index 00000000..7e3a3864 --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-alert.html @@ -0,0 +1,7 @@ +{{ define "fragments/icons/shield-alert" }} + + + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-check.html b/appview/pages/templates/fragments/icons/shield-check.html new file mode 100644 index 00000000..b575192c --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-check.html @@ -0,0 +1,6 @@ +{{ define "fragments/icons/shield-check" }} + + + + +{{ end }} diff --git a/appview/pages/templates/repo/issues/fragments/commentList.html b/appview/pages/templates/repo/issues/fragments/commentList.html index 765c397c..39492331 100644 --- a/appview/pages/templates/repo/issues/fragments/commentList.html +++ b/appview/pages/templates/repo/issues/fragments/commentList.html @@ -14,7 +14,9 @@ "RepoInfo" $root.RepoInfo "LoggedInUser" $root.LoggedInUser "Issue" $root.Issue - "Comment" $comment.Self) }} + "Comment" $comment.Self + "VouchRelationship" (index $root.VouchRelationships $comment.Self.Did) + ) }}
{{ template "topLevelComment" $params }} @@ -28,8 +30,9 @@ "RepoInfo" $root.RepoInfo "LoggedInUser" $root.LoggedInUser "Issue" $root.Issue - "Comment" $reply) - }} + "Comment" $reply + "VouchRelationship" (index $root.VouchRelationships $reply.Did) + ) }}
{{ end }} @@ -41,7 +44,7 @@ {{ define "topLevelComment" }}
- {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} + {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1" .VouchRelationship) }}
{{ template "repo/issues/fragments/issueCommentHeader" . }} @@ -53,7 +56,7 @@ {{ define "replyComment" }}
- {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} + {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1" .VouchRelationship) }}
{{ template "repo/issues/fragments/issueCommentHeader" . }} diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index e17656f8..ce0e9215 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -118,7 +118,9 @@ "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "Issue" $.Issue - "CommentList" $.Issue.CommentList) + "CommentList" $.Issue.CommentList + "VouchRelationships" $.VouchRelationships + ) }} {{ template "repo/issues/fragments/newComment" . }} diff --git a/appview/pages/templates/user/fragments/picLink.html b/appview/pages/templates/user/fragments/picLink.html index 628a05d9..ec447992 100644 --- a/appview/pages/templates/user/fragments/picLink.html +++ b/appview/pages/templates/user/fragments/picLink.html @@ -1,11 +1,11 @@ {{ define "user/fragments/picLink" }} {{ $did := index . 0 }} {{ $classes := index . 1 }} - {{ $vouchRelation := index . 2 }} {{ $handle := resolve $did }} {{ $isVouched := false }} {{ $isDenounced := false }} - {{ if $vouchRelation }} + {{ if ge (len .) 3 }} + {{ $vouchRelation := index . 2 }} {{ if $vouchRelation.IsDirectVouch }} {{ $isVouched = true }} {{ else if $vouchRelation.IsDirectDenounce }} @@ -16,12 +16,12 @@ {{ if $isVouched }} - {{ i "shield" "size-3 text-green-500" }} + {{ template "fragments/icons/shield-check" "size-4" }} {{ else if $isDenounced }} - {{ i "shield" "size-3 text-red-500" }} + {{ template "fragments/icons/shield-alert" "size-4" }} {{ end }} {{ end }} diff --git a/appview/pages/templates/user/fragments/vouch.html b/appview/pages/templates/user/fragments/vouch.html index a21f3c1e..9c88f084 100644 --- a/appview/pages/templates/user/fragments/vouch.html +++ b/appview/pages/templates/user/fragments/vouch.html @@ -17,7 +17,7 @@ {{ $userIdent := "" }} {{ $userDid := "" }} {{ with .VouchRelationship }} - {{ $userIdent = .SubjectDid }} + {{ $userDid = .SubjectDid }} {{ $userIdent = resolve .SubjectDid }} {{ end }}