diff --git a/appview/db/issues.go b/appview/db/issues.go index 029449a5..74231b7a 100644 --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -12,15 +12,19 @@ type Issue struct { OwnerDid string IssueId int IssueAt string - Created *time.Time + Created time.Time Title string Body string Open bool + + // optionally, populate this when querying for reverse mappings + // like comment counts, parent repo etc. Metadata *IssueMetadata } type IssueMetadata struct { CommentCount int + Repo *Repo // labels, assignee etc. } @@ -143,7 +147,7 @@ func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { if err != nil { return nil, err } - issue.Created = &createdTime + issue.Created = createdTime issue.Metadata = &metadata issues = append(issues, issue) @@ -156,7 +160,9 @@ func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { return issues, nil } -func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { +// timeframe here is directly passed into the sql query filter, and any +// timeframe in the past should be negative; e.g.: "-3 months" +func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { var issues []Issue rows, err := e.Query( @@ -168,18 +174,20 @@ func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { i.title, i.body, i.open, - count(c.id) + r.did, + r.name, + r.knot, + r.rkey, + r.created from issues i - left join - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id + join + repos r on i.repo_at = r.at_uri where - i.owner_did = ? - group by - i.id, i.owner_did, i.repo_at, i.issue_id, i.created, i.title, i.body, i.open + i.owner_did = ? and i.created >= date ('now', ?) order by i.created desc`, - ownerDid) + ownerDid, timeframe) if err != nil { return nil, err } @@ -187,19 +195,41 @@ func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { for rows.Next() { var issue Issue - var createdAt string - var metadata IssueMetadata - err := rows.Scan(&issue.OwnerDid, &issue.RepoAt, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) + var issueCreatedAt, repoCreatedAt string + var repo Repo + err := rows.Scan( + &issue.OwnerDid, + &issue.RepoAt, + &issue.IssueId, + &issueCreatedAt, + &issue.Title, + &issue.Body, + &issue.Open, + &repo.Did, + &repo.Name, + &repo.Knot, + &repo.Rkey, + &repoCreatedAt, + ) if err != nil { return nil, err } - createdTime, err := time.Parse(time.RFC3339, createdAt) + issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) if err != nil { return nil, err } - issue.Created = &createdTime - issue.Metadata = &metadata + issue.Created = issueCreatedTime + + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) + if err != nil { + return nil, err + } + repo.Created = repoCreatedTime + + issue.Metadata = &IssueMetadata{ + Repo: &repo, + } issues = append(issues, issue) } @@ -226,7 +256,7 @@ func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { if err != nil { return nil, err } - issue.Created = &createdTime + issue.Created = createdTime return &issue, nil } @@ -246,7 +276,7 @@ func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, [ if err != nil { return nil, nil, err } - issue.Created = &createdTime + issue.Created = createdTime comments, err := GetComments(e, repoAt, issueId) if err != nil { diff --git a/appview/db/profile.go b/appview/db/profile.go index 2e2fccb1..49e9cf03 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -2,69 +2,140 @@ package db import ( "fmt" - "sort" "time" ) -type ProfileTimelineEvent struct { - EventAt time.Time - Type string - *Issue - *Pull - *Repo - - // optional: populate only if Repo is a fork +type RepoEvent struct { + Repo *Repo Source *Repo } -func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) { - timeline := []ProfileTimelineEvent{} - limit := 30 +type ProfileTimeline struct { + ByMonth []ByMonth +} + +type ByMonth struct { + RepoEvents []RepoEvent + IssueEvents IssueEvents + PullEvents PullEvents +} + +func (b ByMonth) IsEmpty() bool { + return len(b.RepoEvents) == 0 && + len(b.IssueEvents.Items) == 0 && + len(b.PullEvents.Items) == 0 +} + +type IssueEvents struct { + Items []*Issue +} + +type IssueEventStats struct { + Open int + Closed int +} + +func (i IssueEvents) Stats() IssueEventStats { + var open, closed int + for _, issue := range i.Items { + if issue.Open { + open += 1 + } else { + closed += 1 + } + } + + return IssueEventStats{ + Open: open, + Closed: closed, + } +} + +type PullEvents struct { + Items []*Pull +} + +func (p PullEvents) Stats() PullEventStats { + var open, merged, closed int + for _, pull := range p.Items { + switch pull.State { + case PullOpen: + open += 1 + case PullMerged: + merged += 1 + case PullClosed: + closed += 1 + } + } + + return PullEventStats{ + Open: open, + Merged: merged, + Closed: closed, + } +} + +type PullEventStats struct { + Closed int + Open int + Merged int +} + +const TimeframeMonths = 3 - pulls, err := GetPullsByOwnerDid(e, forDid) +func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { + timeline := ProfileTimeline{ + ByMonth: make([]ByMonth, TimeframeMonths), + } + currentMonth := time.Now().Month() + timeframe := fmt.Sprintf("-%d months", TimeframeMonths) + + pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) if err != nil { - return timeline, fmt.Errorf("error getting pulls by owner did: %w", err) + return nil, fmt.Errorf("error getting pulls by owner did: %w", err) } + // group pulls by month for _, pull := range pulls { - repo, err := GetRepoByAtUri(e, string(pull.RepoAt)) - if err != nil { - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) + pullMonth := pull.Created.Month() + + if currentMonth-pullMonth > TimeframeMonths { + // shouldn't happen; but times are weird + continue } - timeline = append(timeline, ProfileTimelineEvent{ - EventAt: pull.Created, - Type: "pull", - Pull: &pull, - Repo: repo, - }) + idx := currentMonth - pullMonth + items := &timeline.ByMonth[idx].PullEvents.Items + + *items = append(*items, &pull) } - issues, err := GetIssuesByOwnerDid(e, forDid) + issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) if err != nil { - return timeline, fmt.Errorf("error getting issues by owner did: %w", err) + return nil, fmt.Errorf("error getting issues by owner did: %w", err) } for _, issue := range issues { - repo, err := GetRepoByAtUri(e, string(issue.RepoAt)) - if err != nil { - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) + issueMonth := issue.Created.Month() + + if currentMonth-issueMonth > TimeframeMonths { + // shouldn't happen; but times are weird + continue } - timeline = append(timeline, ProfileTimelineEvent{ - EventAt: *issue.Created, - Type: "issue", - Issue: &issue, - Repo: repo, - }) + idx := currentMonth - issueMonth + items := &timeline.ByMonth[idx].IssueEvents.Items + + *items = append(*items, &issue) } repos, err := GetAllReposByDid(e, forDid) if err != nil { - return timeline, fmt.Errorf("error getting all repos by did: %w", err) + return nil, fmt.Errorf("error getting all repos by did: %w", err) } for _, repo := range repos { + // TODO: get this in the original query; requires COALESCE because nullable var sourceRepo *Repo if repo.Source != "" { sourceRepo, err = GetRepoByAtUri(e, repo.Source) @@ -73,21 +144,21 @@ func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error } } - timeline = append(timeline, ProfileTimelineEvent{ - EventAt: repo.Created, - Type: "repo", - Repo: &repo, - Source: sourceRepo, - }) - } + repoMonth := repo.Created.Month() - sort.Slice(timeline, func(i, j int) bool { - return timeline[i].EventAt.After(timeline[j].EventAt) - }) + if currentMonth-repoMonth > TimeframeMonths { + // shouldn't happen; but times are weird + continue + } - if len(timeline) > limit { - timeline = timeline[:limit] + idx := currentMonth - repoMonth + + items := &timeline.ByMonth[idx].RepoEvents + *items = append(*items, RepoEvent{ + Repo: &repo, + Source: sourceRepo, + }) } - return timeline, nil + return &timeline, nil } diff --git a/appview/db/pulls.go b/appview/db/pulls.go index 2b2e80da..037612db 100644 --- a/appview/db/pulls.go +++ b/appview/db/pulls.go @@ -64,6 +64,9 @@ type Pull struct { // meta Created time.Time PullSource *PullSource + + // optionally, populate this when querying for reverse mappings + Repo *Repo } type PullSource struct { @@ -522,23 +525,32 @@ func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { return &pull, nil } -func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { +// timeframe here is directly passed into the sql query filter, and any +// timeframe in the past should be negative; e.g.: "-3 months" +func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]Pull, error) { var pulls []Pull rows, err := e.Query(` select - owner_did, - repo_at, - pull_id, - created, - title, - state + p.owner_did, + p.repo_at, + p.pull_id, + p.created, + p.title, + p.state, + r.did, + r.name, + r.knot, + r.rkey, + r.created from - pulls + pulls p + join + repos r on p.repo_at = r.at_uri where - owner_did = ? + p.owner_did = ? and p.created >= date ('now', ?) order by - created desc`, did) + p.created desc`, did, timeframe) if err != nil { return nil, err } @@ -546,24 +558,38 @@ func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { for rows.Next() { var pull Pull - var createdAt string + var repo Repo + var pullCreatedAt, repoCreatedAt string err := rows.Scan( &pull.OwnerDid, &pull.RepoAt, &pull.PullId, - &createdAt, + &pullCreatedAt, &pull.Title, &pull.State, + &repo.Did, + &repo.Name, + &repo.Knot, + &repo.Rkey, + &repoCreatedAt, ) if err != nil { return nil, err } - createdTime, err := time.Parse(time.RFC3339, createdAt) + pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) if err != nil { return nil, err } - pull.Created = createdTime + pull.Created = pullCreatedTime + + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) + if err != nil { + return nil, err + } + repo.Created = repoCreatedTime + + pull.Repo = &repo pulls = append(pulls, pull) } diff --git a/appview/db/repos.go b/appview/db/repos.go index 915f48d5..8c201448 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -77,7 +77,9 @@ func GetAllReposByDid(e Execer, did string) ([]Repo, error) { where r.did = ? group by - r.at_uri`, did) + r.at_uri + order by r.created desc`, + did) if err != nil { return nil, err } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 4c99bfd2..a2a19aff 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -178,9 +178,10 @@ type ProfilePageParams struct { CollaboratingRepos []db.Repo ProfileStats ProfileStats FollowStatus db.FollowStatus - DidHandleMap map[string]string AvatarUri string - ProfileTimeline []db.ProfileTimelineEvent + ProfileTimeline *db.ProfileTimeline + + DidHandleMap map[string]string } type ProfileStats struct { diff --git a/appview/pages/templates/repo/blob.html b/appview/pages/templates/repo/blob.html index 071c9052..6736c712 100644 --- a/appview/pages/templates/repo/blob.html +++ b/appview/pages/templates/repo/blob.html @@ -29,7 +29,7 @@ > / {{ else }} - {{ index . 0 }} {{ end }} diff --git a/appview/pages/templates/repo/tree.html b/appview/pages/templates/repo/tree.html index bb619938..6ececaf3 100644 --- a/appview/pages/templates/repo/tree.html +++ b/appview/pages/templates/repo/tree.html @@ -17,11 +17,11 @@ {{ $containerstyle := "py-1" }} {{ $linkstyle := "no-underline hover:underline" }} -
+
- {{ end }} - {{ define "profileTimeline" }} -
-

ACTIVITY

- {{ range .ProfileTimeline }} - {{ if eq .Type "issue" }} -
- {{ $textColor := "text-gray-800 dark:text-gray-400" }} - {{ $icon := "ban" }} - {{ if .Issue.Open }} - {{ $textColor = "text-green-600 dark:text-green-500" }} - {{ $icon = "circle-dot" }} - {{ end }} -
- {{ i $icon "w-5 h-5" }} +

ACTIVITY

+
+ {{ with .ProfileTimeline }} + {{ range $idx, $byMonth := .ByMonth }} + {{ with $byMonth }} +
+ {{ if eq $idx 0 }} + + {{ else }} + {{ $s := "s" }} + {{ if eq $idx 1 }} + {{ $s = "" }} + {{ end }} +

{{$idx}} month{{$s}} ago

+ {{ end }} + + {{ if .IsEmpty }} +
+ No activity for this month
-
-

- {{ .Issue.Title }} #{{ .Issue.IssueId }} - on - {{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }} - -

+ {{ else }} +
+ {{ block "repoEvents" (list .RepoEvents $.DidHandleMap) }} {{ end }} + {{ block "issueEvents" (list .IssueEvents $.DidHandleMap) }} {{ end }} + {{ block "pullEvents" (list .PullEvents $.DidHandleMap) }} {{ end }}
+ {{ end }} +
+ + {{ end }} + {{ else }} +

This user does not have any activity yet.

+ {{ end }} + {{ end }} +
+{{ end }} + +{{ define "repoEvents" }} + {{ $items := index . 0 }} + {{ $handleMap := index . 1 }} + + {{ if gt (len $items) 0 }} +
+ +
+ {{ i "book-plus" "w-4 h-4" }} + created {{ len $items }} {{if eq (len $items) 1 }}repository{{else}}repositories{{end}} +
+
+
+ {{ range $items }} +
+ + {{ if .Source }} + {{ i "git-fork" "w-4 h-4" }} + {{ else }} + {{ i "book-plus" "w-4 h-4" }} + {{ end }} + + + {{- .Repo.Name -}} + +
+ {{ end }} +
+
+ {{ end }} +{{ end }} + +{{ define "issueEvents" }} + {{ $i := index . 0 }} + {{ $items := $i.Items }} + {{ $stats := $i.Stats }} + {{ $handleMap := index . 1 }} + + {{ if gt (len $items) 0 }} +
+ +
+ {{ i "circle-dot" "w-4 h-4" }} + +
+ created {{ len $items }} {{if eq (len $items) 1 }}issue{{else}}issues{{end}}
- {{ else if eq .Type "pull" }} -
- {{ $textColor := "text-gray-800 dark:text-gray-400" }} - {{ $icon := "git-pull-request-closed" }} - {{ if .Pull.State.IsOpen }} - {{ $textColor = "text-green-600 dark:text-green-500" }} - {{ $icon = "git-pull-request" }} - {{ else if .Pull.State.IsMerged }} - {{ $textColor = "text-purple-600 dark:text-purple-500" }} - {{ $icon = "git-merge" }} + + {{ if gt $stats.Open 0 }} + + {{$stats.Open}} open + + {{ end }} + + {{ if gt $stats.Closed 0 }} + + {{$stats.Closed}} closed + + {{ end }} + +
+
+
+ {{ range $items }} + {{ $repoOwner := index $handleMap .Metadata.Repo.Did }} + {{ $repoName := .Metadata.Repo.Name }} + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} + +
+ {{ if .Open }} + + {{ i "circle-dot" "w-4 h-4" }} + + {{ else }} + + {{ i "ban" "w-4 h-4" }} + {{ end }} -
- {{ i $icon "w-5 h-5" }} +
+ #{{ .IssueId }}
- - {{ else if eq .Type "repo" }} -
- {{ if .Source }} -
- {{ i "git-fork" "w-5 h-5" }} -
+ {{ end }} +
+
+ {{ end }} +{{ end }} + +{{ define "pullEvents" }} + {{ $i := index . 0 }} + {{ $items := $i.Items }} + {{ $stats := $i.Stats }} + {{ $handleMap := index . 1 }} + {{ if gt (len $items) 0 }} +
+ +
+ {{ i "git-pull-request" "w-4 h-4" }} + +
+ created {{ len $items }} {{if eq (len $items) 1 }}pull request{{else}}pull requests{{end}} +
+ + {{ if gt $stats.Open 0 }} + + {{$stats.Open}} open + + {{ end }} + + {{ if gt $stats.Merged 0 }} + + {{$stats.Merged}} merged + + {{ end }} + + + {{ if gt $stats.Closed 0 }} + + {{$stats.Closed}} closed + + {{ end }} + +
+
+
+ {{ range $items }} + {{ $repoOwner := index $handleMap .Repo.Did }} + {{ $repoName := .Repo.Name }} + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} + +
+ {{ if .State.IsOpen }} + + {{ i "git-pull-request" "w-4 h-4" }} + + {{ else if .State.IsMerged }} + + {{ i "git-merge" "w-4 h-4" }} + {{ else }} -
- {{ i "book-plus" "w-5 h-5" }} -
+ + {{ i "git-pull-request-closed" "w-4 h-4" }} + {{ end }} -
-

- - {{ if .Source }} - forked - - {{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }} - - to - {{ .Repo.Name }} - {{ else }} - created - {{ .Repo.Name }} - {{ end }} - -

+
+ #{{ .PullId }} +
+
- {{ end }} - {{ else }} -

This user does not have any activity yet.

{{ end }}
+
+ {{ end }} {{ end }} {{ define "profileCard" }} diff --git a/appview/state/profile.go b/appview/state/profile.go index 58d6a4b8..b1189182 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -43,13 +43,19 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { for _, r := range collaboratingRepos { didsToResolve = append(didsToResolve, r.Did) } - for _, evt := range timeline { - if evt.Repo != nil { - if evt.Repo.Source != "" { - didsToResolve = append(didsToResolve, evt.Source.Did) + for _, byMonth := range timeline.ByMonth { + for _, pe := range byMonth.PullEvents.Items { + didsToResolve = append(didsToResolve, pe.Repo.Did) + } + for _, ie := range byMonth.IssueEvents.Items { + didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) + } + for _, re := range byMonth.RepoEvents { + didsToResolve = append(didsToResolve, re.Repo.Did) + if re.Source != nil { + didsToResolve = append(didsToResolve, re.Source.Did) } } - didsToResolve = append(didsToResolve, evt.Repo.Did) } resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) diff --git a/go.mod b/go.mod index 718a5671..281a0b54 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/sethvargo/go-envconfig v1.1.0 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e github.com/yuin/goldmark v1.4.13 - golang.org/x/crypto v0.36.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 ) @@ -107,6 +106,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/time v0.5.0 // indirect