From b7df865e661cd165baf7b14ca7db7f6b6a63c7c3 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Fri, 10 Apr 2026 08:13:37 +0100 Subject: [PATCH] appview/pages: add vouch hats Signed-off-by: oppiliappan --- appview/issues/issues.go | 60 +++++-- appview/models/issue.go | 12 +- appview/models/pull.go | 12 +- appview/models/vouch.go | 43 +++++ appview/notify/db/db.go | 4 +- appview/pages/funcmap.go | 2 + appview/pages/pages.go | 80 ++++++---- .../icons/shield-direct-denounce.html | 5 + .../fragments/icons/shield-direct-vouch.html | 5 + .../fragments/icons/shield-empty.html | 5 + .../icons/shield-indirect-denounce.html | 7 + .../icons/shield-indirect-vouch.html | 6 + .../fragments/icons/shield-mixed.html | 12 ++ .../fragments/icons/shield-plain.html | 5 + .../repo/issues/fragments/commentList.html | 13 +- .../repo/issues/fragments/issueListing.html | 5 +- .../pages/templates/repo/issues/issue.html | 7 +- .../pages/templates/repo/issues/issues.html | 8 +- .../repo/pulls/fragments/pullHeader.html | 3 +- appview/pages/templates/repo/pulls/pull.html | 18 ++- appview/pages/templates/repo/pulls/pulls.html | 5 +- .../user/fragments/networkVouches.html | 42 +++++ .../templates/user/fragments/picLink.html | 52 ++++-- .../templates/user/fragments/vouchButton.html | 30 ++++ .../user/fragments/vouchPopover.html | 151 ++++++++++++++++++ appview/pulls/pulls.go | 45 ++++-- appview/state/profile.go | 80 +++++++++- input.css | 9 ++ 28 files changed, 618 insertions(+), 108 deletions(-) create mode 100644 appview/pages/templates/fragments/icons/shield-direct-denounce.html create mode 100644 appview/pages/templates/fragments/icons/shield-direct-vouch.html create mode 100644 appview/pages/templates/fragments/icons/shield-empty.html create mode 100644 appview/pages/templates/fragments/icons/shield-indirect-denounce.html create mode 100644 appview/pages/templates/fragments/icons/shield-indirect-vouch.html create mode 100644 appview/pages/templates/fragments/icons/shield-mixed.html create mode 100644 appview/pages/templates/fragments/icons/shield-plain.html create mode 100644 appview/pages/templates/user/fragments/networkVouches.html create mode 100644 appview/pages/templates/user/fragments/vouchButton.html create mode 100644 appview/pages/templates/user/fragments/vouchPopover.html diff --git a/appview/issues/issues.go b/appview/issues/issues.go index f2cbf8bb..a96a0185 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -127,21 +127,34 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { return } + vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) + if user != nil { + participants := issue.Participants() + vouchRelationships, err = db.GetVouchRelationshipsBatch(rp.db, syntax.DID(user.Did), participants) + if err != nil { + l.Error("failed to fetch vouch relationships", "err", err) + } + } + defs := make(map[string]*models.LabelDefinition) for _, l := range labelDefs { defs[l.AtUri().String()] = &l } - 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, + err = 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, + VouchRelationships: vouchRelationships, }) + if err != nil { + l.Error("failed to render issue", "err", err) + } } func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { @@ -978,15 +991,28 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { } } + vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) + if user != nil { + dids := make([]syntax.DID, len(issues)) + for i, u := range issues { + dids[i] = syntax.DID(u.Did) + } + vouchRelationships, err = db.GetVouchRelationshipsBatch(rp.db, syntax.DID(user.Did), dids) + if err != nil { + l.Error("failed to fetch vouch relationships", "err", err) + } + } + rp.pages.RepoIssues(w, pages.RepoIssuesParams{ - LoggedInUser: rp.oauth.GetMultiAccountUser(r), - RepoInfo: repoInfo, - Issues: issues, - IssueCount: totalIssues, - LabelDefs: defs, - FilterState: filterState, - FilterQuery: query.String(), - Page: page, + LoggedInUser: rp.oauth.GetMultiAccountUser(r), + RepoInfo: repoInfo, + Issues: issues, + IssueCount: totalIssues, + LabelDefs: defs, + FilterState: filterState, + FilterQuery: query.String(), + Page: page, + VouchRelationships: vouchRelationships, }) } diff --git a/appview/models/issue.go b/appview/models/issue.go index 58cf07a6..84659ae7 100644 --- a/appview/models/issue.go +++ b/appview/models/issue.go @@ -135,21 +135,21 @@ func (i *Issue) CommentList() []CommentListItem { return listing } -func (i *Issue) Participants() []string { - participantSet := make(map[string]struct{}) - participants := []string{} +func (i *Issue) Participants() []syntax.DID { + participantSet := make(map[syntax.DID]struct{}) + participants := []syntax.DID{} - addParticipant := func(did string) { + addParticipant := func(did syntax.DID) { if _, exists := participantSet[did]; !exists { participantSet[did] = struct{}{} participants = append(participants, did) } } - addParticipant(i.Did) + addParticipant(syntax.DID(i.Did)) for _, c := range i.Comments { - addParticipant(c.Did) + addParticipant(syntax.DID(c.Did)) } return participants diff --git a/appview/models/pull.go b/appview/models/pull.go index e39c0bcc..5723b1fa 100644 --- a/appview/models/pull.go +++ b/appview/models/pull.go @@ -386,22 +386,22 @@ func (p *Pull) IsForkBased() bool { return false } -func (p *Pull) Participants() []string { - participantSet := make(map[string]struct{}) - participants := []string{} +func (p *Pull) Participants() []syntax.DID { + participantSet := make(map[syntax.DID]struct{}) + participants := []syntax.DID{} - addParticipant := func(did string) { + addParticipant := func(did syntax.DID) { if _, exists := participantSet[did]; !exists { participantSet[did] = struct{}{} participants = append(participants, did) } } - addParticipant(p.OwnerDid) + addParticipant(syntax.DID(p.OwnerDid)) for _, s := range p.Submissions { for _, sp := range s.Participants() { - addParticipant(sp) + addParticipant(syntax.DID(sp)) } } diff --git a/appview/models/vouch.go b/appview/models/vouch.go index 1ce9d3a9..fa27068f 100644 --- a/appview/models/vouch.go +++ b/appview/models/vouch.go @@ -102,6 +102,49 @@ func (vr *VouchRelationship) GetDirectVouch() *Vouch { return nil } +func (vr *VouchRelationship) IsIndirectVouch() bool { + if vr.IsDirectVouch() || vr.IsDirectDenounce() { + return false + } + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid && v.Kind == VouchKindVouch { + return true + } + } + return false +} + +func (vr *VouchRelationship) IsIndirectDenounce() bool { + if vr.IsDirectVouch() || vr.IsDirectDenounce() { + return false + } + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid && v.Kind == VouchKindDenounce { + return true + } + } + return false +} + +func (vr *VouchRelationship) IsMixed() bool { + if vr.IsDirectVouch() || vr.IsDirectDenounce() { + return false + } + hasVouch := false + hasDenounce := false + for _, v := range vr.NetworkVouches { + if v.Did != vr.ViewerDid { + switch v.Kind { + case VouchKindVouch: + hasVouch = true + case VouchKindDenounce: + hasDenounce = true + } + } + } + return hasVouch && hasDenounce +} + func (vr *VouchRelationship) VouchStrength() int { count := 0 for _, v := range vr.NetworkVouches { diff --git a/appview/notify/db/db.go b/appview/notify/db/db.go index bc5f6658..d3e01ae0 100644 --- a/appview/notify/db/db.go +++ b/appview/notify/db/db.go @@ -305,7 +305,7 @@ func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.P // - remove those already mentioned recipients := sets.Singleton(syntax.DID(repo.Did)) for _, p := range pull.Participants() { - recipients.Insert(syntax.DID(p)) + recipients.Insert(p) } for _, m := range mentions { recipients.Remove(m) @@ -439,7 +439,7 @@ func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, p recipients.Insert(c.SubjectDid) } for _, p := range pull.Participants() { - recipients.Insert(syntax.DID(p)) + recipients.Insert(p) } entityType := "pull" diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index f686381b..9bce3fa6 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -24,6 +24,7 @@ import ( "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/dustin/go-humanize" + "github.com/dustin/go-humanize/english" "github.com/go-enry/go-enry/v2" "github.com/yuin/goldmark" emoji "github.com/yuin/goldmark-emoji" @@ -177,6 +178,7 @@ func (p *Pages) funcMap() template.FuncMap { return s }, "commaFmt": humanize.Comma, + "plural": english.Plural, "relTimeFmt": humanize.Time, "shortRelTimeFmt": func(t time.Time) string { return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{ diff --git a/appview/pages/pages.go b/appview/pages/pages.go index fe4d7c8a..02bf0766 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -624,13 +624,14 @@ func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { } type ProfileCard struct { - UserDid string - HasProfile bool - FollowStatus models.FollowStatus - Punchcard *models.Punchcard - Profile *models.Profile - Stats ProfileStats - Active string + UserDid string + HasProfile bool + FollowStatus models.FollowStatus + VouchRelationship *models.VouchRelationship + Punchcard *models.Punchcard + Profile *models.Profile + Stats ProfileStats + Active string } type ProfileStats struct { @@ -647,6 +648,7 @@ func (p *ProfileCard) GetTabs() [][]any { {"repos", "repos", "book-marked", p.Stats.RepoCount}, {"starred", "starred", "star", p.Stats.StarredCount}, {"strings", "strings", "line-squiggle", p.Stats.StringCount}, + {"vouches", "vouches", "shield", nil}, } return tabs @@ -708,6 +710,20 @@ func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { return p.executeProfile("user/strings", w, params) } +type ProfileVouchesParams struct { + LoggedInUser *oauth.MultiAccountUser + Vouches []models.Vouch + Suggestions []models.VouchSuggestion + Card *ProfileCard + Page pagination.Page + Active string +} + +func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { + params.Active = "vouches" + return p.executeProfile("user/vouches", w, params) +} + type FollowCard struct { UserDid string LoggedInUser *oauth.MultiAccountUser @@ -1125,15 +1141,16 @@ func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) err } type RepoIssuesParams struct { - LoggedInUser *oauth.MultiAccountUser - RepoInfo repoinfo.RepoInfo - Active string - Issues []models.Issue - IssueCount int - LabelDefs map[string]*models.LabelDefinition - Page pagination.Page - FilterState string - FilterQuery string + LoggedInUser *oauth.MultiAccountUser + RepoInfo repoinfo.RepoInfo + Active string + Issues []models.Issue + IssueCount int + LabelDefs map[string]*models.LabelDefinition + Page pagination.Page + FilterState string + FilterQuery string + VouchRelationships map[syntax.DID]*models.VouchRelationship } func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { @@ -1150,8 +1167,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[syntax.DID]*models.VouchRelationship } func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { @@ -1259,17 +1277,18 @@ func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { } type RepoPullsParams struct { - LoggedInUser *oauth.MultiAccountUser - RepoInfo repoinfo.RepoInfo - Pulls []*models.Pull - Active string - FilterState string - FilterQuery string - Stacks []models.Stack - Pipelines map[string]models.Pipeline - LabelDefs map[string]*models.LabelDefinition - Page pagination.Page - PullCount int + LoggedInUser *oauth.MultiAccountUser + RepoInfo repoinfo.RepoInfo + Pulls []*models.Pull + Active string + FilterState string + FilterQuery string + Stacks []models.Stack + Pipelines map[string]models.Pipeline + LabelDefs map[string]*models.LabelDefinition + Page pagination.Page + PullCount int + VouchRelationships map[syntax.DID]*models.VouchRelationship } func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { @@ -1314,7 +1333,8 @@ type RepoSinglePullParams struct { Reactions map[models.ReactionKind]models.ReactionDisplayData UserReacted map[models.ReactionKind]bool - LabelDefs map[string]*models.LabelDefinition + LabelDefs map[string]*models.LabelDefinition + VouchRelationships map[syntax.DID]*models.VouchRelationship } func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { diff --git a/appview/pages/templates/fragments/icons/shield-direct-denounce.html b/appview/pages/templates/fragments/icons/shield-direct-denounce.html new file mode 100644 index 00000000..0e18a59e --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-direct-denounce.html @@ -0,0 +1,5 @@ +{{ define "fragments/icons/shield-direct-denounce" }} + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-direct-vouch.html b/appview/pages/templates/fragments/icons/shield-direct-vouch.html new file mode 100644 index 00000000..64fb9abc --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-direct-vouch.html @@ -0,0 +1,5 @@ +{{ define "fragments/icons/shield-direct-vouch" }} + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-empty.html b/appview/pages/templates/fragments/icons/shield-empty.html new file mode 100644 index 00000000..afe078cb --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-empty.html @@ -0,0 +1,5 @@ +{{ define "fragments/icons/shield-empty" }} + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-indirect-denounce.html b/appview/pages/templates/fragments/icons/shield-indirect-denounce.html new file mode 100644 index 00000000..84590ccb --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-indirect-denounce.html @@ -0,0 +1,7 @@ +{{ define "fragments/icons/shield-indirect-denounce" }} + + + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-indirect-vouch.html b/appview/pages/templates/fragments/icons/shield-indirect-vouch.html new file mode 100644 index 00000000..9aa0ed49 --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-indirect-vouch.html @@ -0,0 +1,6 @@ +{{ define "fragments/icons/shield-indirect-vouch" }} + + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-mixed.html b/appview/pages/templates/fragments/icons/shield-mixed.html new file mode 100644 index 00000000..5870e20f --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-mixed.html @@ -0,0 +1,12 @@ +{{ define "fragments/icons/shield-mixed" }} + + + + + + + + + + +{{ end }} diff --git a/appview/pages/templates/fragments/icons/shield-plain.html b/appview/pages/templates/fragments/icons/shield-plain.html new file mode 100644 index 00000000..c7fb9636 --- /dev/null +++ b/appview/pages/templates/fragments/icons/shield-plain.html @@ -0,0 +1,5 @@ +{{ define "fragments/icons/shield-plain" }} + + + +{{ 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/fragments/issueListing.html b/appview/pages/templates/repo/issues/fragments/issueListing.html index 3d892b53..39fafb07 100644 --- a/appview/pages/templates/repo/issues/fragments/issueListing.html +++ b/appview/pages/templates/repo/issues/fragments/issueListing.html @@ -26,8 +26,9 @@ {{ $state }} - - {{ template "user/fragments/picHandleLink" .Did }} + + {{ template "user/fragments/picLink" (list .Did "size-6" (index $.VouchRelationships .Did)) }} + {{ resolve .Did }} diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index e17656f8..9b317552 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -68,7 +68,8 @@ opened by - {{ template "user/fragments/picHandleLink" .Issue.Did }} + {{ template "user/fragments/picLink" (list .Issue.Did "size-6" (index .VouchRelationships .Issue.Did)) }} + {{ resolve .Issue.Did }} {{ if .Issue.Edited }} edited {{ template "repo/fragments/time" .Issue.Edited }} @@ -118,7 +119,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/repo/issues/issues.html b/appview/pages/templates/repo/issues/issues.html index ad9ea64e..3dbd89f0 100644 --- a/appview/pages/templates/repo/issues/issues.html +++ b/appview/pages/templates/repo/issues/issues.html @@ -65,7 +65,13 @@ {{ define "repoAfter" }}
- {{ template "repo/issues/fragments/issueListing" (dict "Issues" .Issues "RepoPrefix" .RepoInfo.FullName "LabelDefs" .LabelDefs) }} + {{ template "repo/issues/fragments/issueListing" + (dict + "Issues" .Issues + "RepoPrefix" .RepoInfo.FullName + "LabelDefs" .LabelDefs + "VouchRelationships" .VouchRelationships + ) }}
{{if gt .IssueCount .Page.Limit }} {{ template "fragments/pagination" (dict diff --git a/appview/pages/templates/repo/pulls/fragments/pullHeader.html b/appview/pages/templates/repo/pulls/fragments/pullHeader.html index bd670842..36b6aae6 100644 --- a/appview/pages/templates/repo/pulls/fragments/pullHeader.html +++ b/appview/pages/templates/repo/pulls/fragments/pullHeader.html @@ -11,7 +11,8 @@ {{ template "repo/pulls/fragments/pullState" .Pull.State }} opened by - {{ template "user/fragments/picHandleLink" .Pull.OwnerDid }} + {{ template "user/fragments/picLink" (list .Pull.OwnerDid "size-6" (index .VouchRelationships .Pull.OwnerDid)) }} + {{ resolve .Pull.OwnerDid }} {{ template "repo/fragments/time" .Pull.Created }} diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html index 6d0773ab..177d7148 100644 --- a/appview/pages/templates/repo/pulls/pull.html +++ b/appview/pages/templates/repo/pulls/pull.html @@ -300,7 +300,7 @@ flex gap-2 sticky top-0 z-20">
- {{ template "user/fragments/picLink" (list $root.Pull.OwnerDid "size-8") }} + {{ template "user/fragments/picLink" (list $root.Pull.OwnerDid "size-8" (index $root.VouchRelationships $root.Pull.OwnerDid)) }}
@@ -592,7 +592,7 @@
{{ range $item.Comments }} - {{ template "submissionComment" . }} + {{ template "submissionComment" (list . $root) }} {{ end }}
{{ if gt $c 0}} @@ -625,25 +625,27 @@ {{ end }} {{ define "submissionComment" }} -
+ {{ $comment := index . 0 }} + {{ $root := index . 1 }} +
- {{ template "user/fragments/picLink" (list .OwnerDid "size-8") }} + {{ template "user/fragments/picLink" (list $comment.OwnerDid "size-8" (index $root.VouchRelationships $comment.OwnerDid)) }}
- {{ .Body | markdown }} + {{ $comment.Body | markdown }}
diff --git a/appview/pages/templates/repo/pulls/pulls.html b/appview/pages/templates/repo/pulls/pulls.html index f0bc0f1e..58b99b5b 100644 --- a/appview/pages/templates/repo/pulls/pulls.html +++ b/appview/pages/templates/repo/pulls/pulls.html @@ -81,8 +81,9 @@
{{ template "repo/pulls/fragments/pullState" $topPR.State }} - - {{ template "user/fragments/picHandleLink" $topPR.OwnerDid }} + + {{ template "user/fragments/picLink" (list $topPR.OwnerDid "size-6" (index $.VouchRelationships $topPR.OwnerDid)) }} + {{ resolve $topPR.OwnerDid }} diff --git a/appview/pages/templates/user/fragments/networkVouches.html b/appview/pages/templates/user/fragments/networkVouches.html new file mode 100644 index 00000000..a8afe576 --- /dev/null +++ b/appview/pages/templates/user/fragments/networkVouches.html @@ -0,0 +1,42 @@ +{{/* Takes a VouchRelationship directly */}} +{{ define "user/fragments/networkVouches" }} + {{ $vouches := (list) }} + {{ $denounces := (list) }} + {{ range .IndirectVouches }} + {{ if .IsVouch }} + {{ $vouches = append $vouches .Did }} + {{ else if .IsDenounce }} + {{ $denounces = append $denounces .Did }} + {{ end }} + {{ end }} + +
+ {{ if $vouches }} +
+ + vouched for by {{ len $vouches }} {{ if eq (len $vouches) 1 }}user{{ else }}users{{ end }} + + {{ template "fragments/tinyAvatarList" (dict "all" $vouches "classes" "size-6") }} +
+ {{ end }} + + {{ if $denounces }} +
+ + denounced by {{ len $denounces }} {{ if eq (len $denounces) 1 }}user{{ else }}users{{ end }} + + {{ template "fragments/tinyAvatarList" (dict "all" $denounces "classes" "size-6") }} +
+ {{ end }} +
+{{ end }} diff --git a/appview/pages/templates/user/fragments/picLink.html b/appview/pages/templates/user/fragments/picLink.html index 096c7d9f..073af802 100644 --- a/appview/pages/templates/user/fragments/picLink.html +++ b/appview/pages/templates/user/fragments/picLink.html @@ -2,14 +2,46 @@ {{ $did := index . 0 }} {{ $classes := index . 1 }} {{ $handle := resolve $did }} - - - + {{ $vr := false }} + {{ if ge (len .) 3 }} + {{ $vr = index . 2 }} + {{ end }} +
+ + + + {{ if $vr }} + {{ if ne $did $vr.ViewerDid }} + + {{ end }} + {{ end }} +
+ {{ if $vr }} + {{ if ne $did $vr.ViewerDid }} + {{ template "user/fragments/vouchPopover" (dict "VouchRelationship" $vr) }} + {{ end }} + {{ end }} {{ end }} - - - diff --git a/appview/pages/templates/user/fragments/vouchButton.html b/appview/pages/templates/user/fragments/vouchButton.html new file mode 100644 index 00000000..dbc76e84 --- /dev/null +++ b/appview/pages/templates/user/fragments/vouchButton.html @@ -0,0 +1,30 @@ +{{ define "user/fragments/vouchButton" }} +{{ $isVouched := false }} +{{ $isDenounced := false }} +{{ if .VouchRelationship }} + {{ if .VouchRelationship.IsDirectVouch }} + {{ $isVouched = true }} + {{ else if .VouchRelationship.IsDirectDenounce }} + {{ $isDenounced = true }} + {{ end }} +{{ end }} + +{{ $userDid := "" }} +{{ with .VouchRelationship }} + {{ $userDid = .SubjectDid }} +{{ end }} + +{{ end }} diff --git a/appview/pages/templates/user/fragments/vouchPopover.html b/appview/pages/templates/user/fragments/vouchPopover.html new file mode 100644 index 00000000..f98f3fa2 --- /dev/null +++ b/appview/pages/templates/user/fragments/vouchPopover.html @@ -0,0 +1,151 @@ +{{ define "user/fragments/vouchPopover" }} +{{ $isVouched := false }} +{{ $isDenounced := false }} +{{ $isUndecided := false }} +{{ if .VouchRelationship }} + {{ if .VouchRelationship.IsDirectVouch }} + {{ $isVouched = true }} + {{ else if .VouchRelationship.IsDirectDenounce }} + {{ $isDenounced = true }} + {{ else }} + {{ $isUndecided = true }} + {{ end }} +{{ else }} + {{ $isUndecided = true }} +{{ end }} + +{{ $userIdent := "" }} +{{ $userDid := "" }} +{{ with .VouchRelationship }} + {{ $userDid = .SubjectDid }} + {{ $userIdent = resolve .SubjectDid }} +{{ end }} +
+ +
+ +
+

Vouch for {{ $userIdent }}

+

+ {{ with .VouchRelationship }} + {{ with .GetDirectVouch }} + You {{if $isVouched}}vouched{{else}}denounced{{end}} {{ $userIdent }} {{ relTimeFmt .CreatedAt }}. + You can change your descision below. + {{ else }} + Vouching builds a web-of-trust across Tangled. Vouch for users you + have had positive interactions with. + {{ end }} + {{ end }} +

+
+ + {{ if .VouchRelationship }} + {{ if .VouchRelationship.IndirectVouches }} +
+

From your network:

+ {{ template "user/fragments/networkVouches" .VouchRelationship }} +
+ {{ end }} + {{ end }} + + + + {{ $labelClass := "grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded p-2 py- ring-1 ring-gray-200 dark:ring-gray-700 cursor-pointer" }} + +
+ + + + + + + + + +
+ +
+ + +
+
+ +
+
+{{ end }} diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 3faf8495..d6bb8d01 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -266,6 +266,15 @@ func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff defs[l.AtUri().String()] = &l } + vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) + if user != nil { + participants := pull.Participants() + vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), participants) + if err != nil { + l.Error("failed to fetch vouch relationships", "err", err) + } + } + patch := pull.Submissions[roundIdInt].CombinedPatch() var diff types.DiffRenderer diff = patchutil.AsNiceDiff(patch, pull.TargetBranch) @@ -306,7 +315,8 @@ func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff Reactions: reactionMap, UserReacted: userReactions, - LabelDefs: defs, + LabelDefs: defs, + VouchRelationships: vouchRelationships, }) } @@ -782,17 +792,30 @@ func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { filterState = state.String() } + vouchRelationships := make(map[syntax.DID]*models.VouchRelationship) + if user != nil { + dids := make([]syntax.DID, len(pulls)) + for i, p := range pulls { + dids[i] = syntax.DID(p.OwnerDid) + } + vouchRelationships, err = db.GetVouchRelationshipsBatch(s.db, syntax.DID(user.Did), dids) + if err != nil { + l.Error("failed to fetch vouch relationships", "err", err) + } + } + s.pages.RepoPulls(w, pages.RepoPullsParams{ - LoggedInUser: s.oauth.GetMultiAccountUser(r), - RepoInfo: repoInfo, - Pulls: pulls, - LabelDefs: defs, - FilterState: filterState, - FilterQuery: query.String(), - Stacks: stacks, - Pipelines: m, - Page: page, - PullCount: totalPulls, + LoggedInUser: s.oauth.GetMultiAccountUser(r), + RepoInfo: repoInfo, + Pulls: pulls, + LabelDefs: defs, + FilterState: filterState, + FilterQuery: query.String(), + Stacks: stacks, + Pipelines: m, + Page: page, + PullCount: totalPulls, + VouchRelationships: vouchRelationships, }) } diff --git a/appview/state/profile.go b/appview/state/profile.go index 8f2d5468..88600455 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -43,6 +43,10 @@ func (s *State) Profile(w http.ResponseWriter, r *http.Request) { ServeHTTP(w, r) case "strings": s.stringsPage(w, r) + case "vouches": + middleware. + Paginate(http.HandlerFunc(s.vouchesPage)). + ServeHTTP(w, r) default: s.profileOverview(w, r) } @@ -93,9 +97,12 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { loggedInUser := s.oauth.GetMultiAccountUser(r) followStatus := models.IsNotFollowing var loggedInDid string + var vouchRelationship *models.VouchRelationship + if loggedInUser != nil { followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) loggedInDid = loggedInUser.Did + vouchRelationship, err = db.GetVouchRelationship(s.db, syntax.DID(loggedInUser.Did), syntax.DID(did)) } showPunchcard := s.shouldShowPunchcard(did, loggedInDid) @@ -116,10 +123,11 @@ func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { } return &pages.ProfileCard{ - UserDid: did, - HasProfile: hasProfile, - Profile: profile, - FollowStatus: followStatus, + UserDid: did, + HasProfile: hasProfile, + Profile: profile, + FollowStatus: followStatus, + VouchRelationship: vouchRelationship, Stats: pages.ProfileStats{ RepoCount: repoCount, StringCount: stringCount, @@ -177,13 +185,16 @@ func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { l.Error("failed to create timeline", "err", err) } - s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ + err = s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ LoggedInUser: s.oauth.GetMultiAccountUser(r), Card: profile, Repos: pinnedRepos, CollaboratingRepos: pinnedCollaboratingRepos, ProfileTimeline: timeline, }) + if err != nil { + l.Error("failed to render template", "err", err) + } } func (s *State) shouldShowPunchcard(targetDid, requesterDid string) bool { @@ -380,6 +391,65 @@ func (s *State) stringsPage(w http.ResponseWriter, r *http.Request) { }) } +func (s *State) vouchesPage(w http.ResponseWriter, r *http.Request) { + l := s.logger.With("handler", "vouchesPage") + + profile, err := s.profile(r) + if err != nil { + l.Error("failed to build profile card", "err", err) + s.pages.Error500(w) + return + } + l = l.With("profileDid", profile.UserDid) + + loggedInUser := s.oauth.GetMultiAccountUser(r) + page := pagination.FromContext(r.Context()) + + var vouches []models.Vouch + if loggedInUser != nil { + vouches, err = db.GetNetworkVouchTimeline(s.db, loggedInUser.Did, profile.UserDid, page) + if err != nil { + l.Error("failed to get vouch timeline", "err", err) + s.pages.Error500(w) + return + } + } + + var suggestions []models.VouchSuggestion + if loggedInUser != nil && loggedInUser.Did == profile.UserDid { + suggestions, err = db.GetVouchSuggestions(s.db, profile.UserDid, 5) + if err != nil { + l.Error("failed to get vouch suggestions", "err", err) + } + + if len(suggestions) > 0 { + suggestionDids := make([]syntax.DID, len(suggestions)) + for i, s := range suggestions { + suggestionDids[i] = syntax.DID(s.Did) + } + relationships, err := db.GetVouchRelationshipsBatch(s.db, syntax.DID(loggedInUser.Did), suggestionDids) + if err != nil { + l.Error("failed to get vouch relationships for suggestions", "err", err) + } else { + for i := range suggestions { + suggestions[i].VouchRelationship = relationships[suggestions[i].Did] + } + } + } + } + + err = s.pages.ProfileVouches(w, pages.ProfileVouchesParams{ + LoggedInUser: loggedInUser, + Vouches: vouches, + Suggestions: suggestions, + Card: profile, + Page: page, + }) + if err != nil { + l.Error("failed to render page", "err", err) + } +} + type FollowsPageParams struct { Follows []pages.FollowCard Card *pages.ProfileCard diff --git a/input.css b/input.css index dc094b35..023b7866 100644 --- a/input.css +++ b/input.css @@ -147,6 +147,15 @@ disabled:before:bg-green-400 dark:disabled:before:bg-green-600; } + .btn-cancel { + @apply btn text-white + before:bg-red-600 hover:before:bg-red-700 + dark:before:bg-red-700 dark:hover:before:bg-red-800 + before:border before:border-red-700 hover:before:border-red-800 + focus-visible:before:outline-red-500 + disabled:before:bg-red-400 dark:disabled:before:bg-red-600; + } + .prose { overflow-wrap: anywhere; } -- 2.51.2