forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

group profile timeline by month

Changed files
+401 -90
appview
+48 -18
appview/db/issues.go
···
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.
}
···
if err != nil {
return nil, err
}
-
issue.Created = &createdTime
+
issue.Created = createdTime
issue.Metadata = &metadata
issues = append(issues, issue)
···
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(
···
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
}
···
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 = issueCreatedTime
+
+
repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt)
if err != nil {
return nil, err
}
-
issue.Created = &createdTime
-
issue.Metadata = &metadata
+
repo.Created = repoCreatedTime
+
+
issue.Metadata = &IssueMetadata{
+
Repo: &repo,
+
}
issues = append(issues, issue)
}
···
if err != nil {
return nil, err
}
-
issue.Created = &createdTime
+
issue.Created = createdTime
return &issue, nil
}
···
if err != nil {
return nil, nil, err
}
-
issue.Created = &createdTime
+
issue.Created = createdTime
comments, err := GetComments(e, repoAt, issueId)
if err != nil {
+117 -48
appview/db/profile.go
···
package db
import (
+
"encoding/json"
"fmt"
-
"sort"
"time"
)
-
type ProfileTimelineEvent struct {
-
EventAt time.Time
-
Type string
-
*Issue
-
*Pull
-
*Repo
+
type RepoEvent struct {
+
Repo *Repo
+
Source *Repo
+
}
+
+
type ProfileTimeline struct {
+
ByMonth []ByMonth
+
}
+
+
type ByMonth struct {
+
RepoEvents []RepoEvent
+
IssueEvents IssueEvents
+
PullEvents PullEvents
+
}
+
+
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,
+
}
+
}
-
// optional: populate only if Repo is a fork
-
Source *Repo
+
type PullEventStats struct {
+
Closed int
+
Open int
+
Merged int
}
-
func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) {
-
timeline := []ProfileTimelineEvent{}
-
limit := 30
+
const TimeframeMonths = 3
+
+
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)
+
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)
···
}
}
-
timeline = append(timeline, ProfileTimelineEvent{
-
EventAt: repo.Created,
-
Type: "repo",
-
Repo: &repo,
-
Source: sourceRepo,
+
repoMonth := repo.Created.Month()
+
+
if currentMonth-repoMonth > TimeframeMonths {
+
// shouldn't happen; but times are weird
+
continue
+
}
+
+
idx := currentMonth - repoMonth
+
+
items := &timeline.ByMonth[idx].RepoEvents
+
*items = append(*items, RepoEvent{
+
Repo: &repo,
+
Source: sourceRepo,
})
}
-
sort.Slice(timeline, func(i, j int) bool {
-
return timeline[i].EventAt.After(timeline[j].EventAt)
-
})
-
-
if len(timeline) > limit {
-
timeline = timeline[:limit]
-
}
+
x, _ := json.MarshalIndent(timeline, "", "\t")
+
fmt.Println(string(x))
-
return timeline, nil
+
return &timeline, nil
}
+40 -14
appview/db/pulls.go
···
// meta
Created time.Time
PullSource *PullSource
+
+
// optionally, populate this when querying for reverse mappings
+
Repo *Repo
}
type PullSource struct {
···
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
}
···
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)
}
+3 -1
appview/db/repos.go
···
where
r.did = ?
group by
-
r.at_uri`, did)
+
r.at_uri
+
order by r.created desc`,
+
did)
if err != nil {
return nil, err
}
+3 -2
appview/pages/pages.go
···
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 {
+179 -2
appview/pages/templates/user/profile.html
···
{{ block "ownRepos" . }}{{ end }}
{{ block "collaboratingRepos" . }}{{ end }}
</div>
-
<div class="md:col-span-2 order-3 md:order-3">
-
{{ block "profileTimeline" . }}{{ end }}
+
{{ block "profileTimeline2" . }}{{ end }}
</div>
</div>
{{ end }}
+
{{ define "profileTimeline2" }}
+
<p class="text-sm font-bold py-2 dark:text-white">ACTIVITY</p>
+
<div class="flex flex-col gap-6 relative">
+
{{ with .ProfileTimeline }}
+
{{ range $idx, $byMonth := .ByMonth }}
+
{{ with $byMonth }}
+
<div>
+
{{ if eq $idx 0 }}
+
<p class="text-sm font-bold py-2 dark:text-white">This month</p>
+
{{ else }}
+
{{ $s := "s" }}
+
{{ if eq $idx 1 }}
+
{{ $s = "" }}
+
{{ end }}
+
<p class="text-sm font-bold py-2 dark:text-white">{{$idx}} month{{$s}} ago</p>
+
{{ end }}
+
+
<div class="flex flex-col gap-4">
+
{{ block "repoEvents" (list .RepoEvents $.DidHandleMap) }} {{ end }}
+
{{ block "issueEvents" (list .IssueEvents $.DidHandleMap) }} {{ end }}
+
{{ block "pullEvents" (list .PullEvents $.DidHandleMap) }} {{ end }}
+
</div>
+
</div>
+
+
{{ end }}
+
{{ else }}
+
<p class="dark:text-white">This user does not have any activity yet.</p>
+
{{ end }}
+
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "repoEvents" }}
+
{{ $items := index . 0 }}
+
{{ $handleMap := index . 1 }}
+
+
{{ if gt (len $items) 0 }}
+
<details open>
+
<summary class="list-none cursor-pointer">
+
<div class="flex items-center gap-2">
+
{{ i "unfold-vertical" "w-4 h-4" }}
+
created {{ len $items }} repositories
+
</div>
+
</summary>
+
<div class="p-2 pl-8 text-sm flex flex-col gap-3">
+
{{ range $items }}
+
<div class="flex flex-wrap items-center gap-2">
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ if .Source }}
+
{{ i "git-fork" "w-4 h-4" }}
+
{{ else }}
+
{{ i "book-plus" "w-4 h-4" }}
+
{{ end }}
+
</span>
+
<a href="/{{ index $handleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">
+
{{- .Repo.Name -}}
+
</a>
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time>
+
</div>
+
{{ end }}
+
</div>
+
</details>
+
{{ end }}
+
{{ end }}
+
+
{{ define "issueEvents" }}
+
{{ $i := index . 0 }}
+
{{ $items := $i.Items }}
+
{{ $stats := $i.Stats }}
+
{{ $handleMap := index . 1 }}
+
+
{{ if gt (len $items) 0 }}
+
<details open>
+
<summary class="list-none cursor-pointer">
+
<div class="flex items-center gap-2">
+
{{ i "unfold-vertical" "w-4 h-4" }}
+
<span>
+
created {{ len $items }} issues
+
</span>
+
<span class="px-2 py-1/2 text-sm rounded-sm text-white bg-green-600 dark:bg-green-700">
+
{{$stats.Open}} open
+
</span>
+
<span class="px-2 py-1/2 text-sm rounded-sm text-white bg-gray-800 dark:bg-gray-700">
+
{{$stats.Closed}} closed
+
</span>
+
</div>
+
</summary>
+
<div class="p-2 pl-8 text-sm flex flex-col gap-3">
+
{{ range $items }}
+
{{ $repoOwner := index $handleMap .Metadata.Repo.Did }}
+
{{ $repoName := .Metadata.Repo.Name }}
+
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
+
+
<div class="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300">
+
{{ if .Open }}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "circle-dot" "w-4 h-4" }}
+
</span>
+
{{ else }}
+
<span class="text-gray-500 dark:text-gray-400">
+
{{ i "ban" "w-4 h-4" }}
+
</span>
+
{{ end }}
+
<a href="/{{$repoUrl}}/issues/{{ .IssueId }}" class="no-underline hover:underline">
+
<span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span>
+
{{ .Title -}}
+
</a>
+
on
+
<a href="/{{$repoUrl}}" class="no-underline hover:underline">
+
{{$repoUrl}}
+
</a>
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Created | shortTimeFmt }}</time>
+
</p>
+
{{ end }}
+
</div>
+
</details>
+
{{ end }}
+
{{ end }}
+
+
{{ define "pullEvents" }}
+
{{ $i := index . 0 }}
+
{{ $items := $i.Items }}
+
{{ $stats := $i.Stats }}
+
{{ $handleMap := index . 1 }}
+
{{ if gt (len $items) 0 }}
+
<details open>
+
<summary class="list-none cursor-pointer">
+
<div class="flex items-center gap-2">
+
{{ i "unfold-vertical" "w-4 h-4" }}
+
<span>
+
created {{ len $items }} pull requests
+
</span>
+
<span class="px-2 py-1/2 text-sm rounded-sm text-white bg-green-600 dark:bg-green-700">
+
{{$stats.Open}} open
+
</span>
+
<span class="px-2 py-1/2 text-sm rounded-sm text-white bg-purple-600 dark:bg-purple-700">
+
{{$stats.Merged}} merged
+
</span>
+
<span class="px-2 py-1/2 text-sm rounded-sm text-black dark:text-white bg-gray-50 dark:bg-gray-700 ">
+
{{$stats.Closed}} closed
+
</span>
+
</div>
+
</summary>
+
<div class="p-2 pl-8 text-sm flex flex-col gap-3">
+
{{ range $items }}
+
{{ $repoOwner := index $handleMap .Repo.Did }}
+
{{ $repoName := .Repo.Name }}
+
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
+
+
<div class="flex flex-wrap items-center gap-2 text-gray-600 dark:text-gray-300">
+
{{ if .State.IsOpen }}
+
<span class="text-green-600 dark:text-green-500">
+
{{ i "git-pull-request" "w-4 h-4" }}
+
</span>
+
{{ else if .State.IsMerged }}
+
<span class="text-purple-600 dark:text-purple-500">
+
{{ i "git-merge" "w-4 h-4" }}
+
</span>
+
{{ else }}
+
<span class="text-gray-600 dark:text-gray-300">
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
+
</span>
+
{{ end }}
+
<a href="/{{$repoUrl}}/pulls/{{ .PullId }}" class="no-underline hover:underline">
+
<span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span>
+
{{ .Title -}}
+
</a>
+
on
+
<a href="/{{$repoUrl}}" class="no-underline hover:underline">
+
{{$repoUrl}}
+
</a>
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Created | shortTimeFmt }}</time>
+
</p>
+
{{ end }}
+
</div>
+
</details>
+
{{ end }}
+
{{ end }}
{{ define "profileTimeline" }}
<div class="flex flex-col gap-3 relative">
+11 -5
appview/state/profile.go
···
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)