From e52b67e89797be85491c7b0b8488f10ba3ccf180 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Fri, 28 Mar 2025 18:05:56 +0200 Subject: [PATCH] appview: profile: activity timeline --- appview/db/issues.go | 65 ++++++++++++++-- appview/db/profile.go | 75 +++++++++++++++++++ appview/db/pulls.go | 53 +++++++++++++ appview/pages/pages.go | 1 + appview/pages/templates/user/profile.html | 89 ++++++++++++++++++---- appview/state/profile.go | 91 +++++++++++++++++++++++ appview/state/state.go | 69 ----------------- 7 files changed, 354 insertions(+), 89 deletions(-) create mode 100644 appview/db/profile.go create mode 100644 appview/state/profile.go diff --git a/appview/db/issues.go b/appview/db/issues.go index 9de11bff..029449a5 100644 --- a/appview/db/issues.go +++ b/appview/db/issues.go @@ -118,7 +118,7 @@ func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { issues i left join comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id - where + where i.repo_at = ? and i.open = ? group by i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open @@ -156,6 +156,61 @@ func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { return issues, nil } +func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { + var issues []Issue + + rows, err := e.Query( + `select + i.owner_did, + i.repo_at, + i.issue_id, + i.created, + i.title, + i.body, + i.open, + count(c.id) + from + issues i + left join + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id + 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 + order by + i.created desc`, + ownerDid) + if err != nil { + return nil, err + } + defer rows.Close() + + 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) + if err != nil { + return nil, err + } + + createdTime, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + return nil, err + } + issue.Created = &createdTime + issue.Metadata = &metadata + + issues = append(issues, issue) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return issues, nil +} + func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` row := e.QueryRow(query, repoAt, issueId) @@ -219,7 +274,7 @@ func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) var comments []Comment rows, err := e.Query(` - select + select owner_did, issue_id, comment_id, @@ -230,8 +285,8 @@ func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) deleted from comments - where - repo_at = ? and issue_id = ? + where + repo_at = ? and issue_id = ? order by created asc`, repoAt, @@ -354,7 +409,7 @@ func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error { _, err := e.Exec( ` - update comments + update comments set body = "", deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where repo_at = ? and issue_id = ? and comment_id = ? diff --git a/appview/db/profile.go b/appview/db/profile.go new file mode 100644 index 00000000..abc905e4 --- /dev/null +++ b/appview/db/profile.go @@ -0,0 +1,75 @@ +package db + +import ( + "sort" + "time" +) + +type ProfileTimelineEvent struct { + EventAt time.Time + Type string + *Issue + *Pull + *Repo +} + +func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) { + timeline := []ProfileTimelineEvent{} + + pulls, err := GetPullsByOwnerDid(e, forDid) + if err != nil { + return timeline, err + } + + for _, pull := range pulls { + repo, err := GetRepoByAtUri(e, string(pull.RepoAt)) + if err != nil { + return timeline, err + } + + timeline = append(timeline, ProfileTimelineEvent{ + EventAt: pull.Created, + Type: "pull", + Pull: &pull, + Repo: repo, + }) + } + + issues, err := GetIssuesByOwnerDid(e, forDid) + if err != nil { + return timeline, err + } + + for _, issue := range issues { + repo, err := GetRepoByAtUri(e, string(issue.RepoAt)) + if err != nil { + return timeline, err + } + + timeline = append(timeline, ProfileTimelineEvent{ + EventAt: *issue.Created, + Type: "issue", + Issue: &issue, + Repo: repo, + }) + } + + repos, err := GetAllReposByDid(e, forDid) + if err != nil { + return timeline, err + } + + for _, repo := range repos { + timeline = append(timeline, ProfileTimelineEvent{ + EventAt: repo.Created, + Type: "repo", + Repo: &repo, + }) + } + + sort.Slice(timeline, func(i, j int) bool { + return timeline[i].EventAt.After(timeline[j].EventAt) + }) + + return timeline, nil +} diff --git a/appview/db/pulls.go b/appview/db/pulls.go index 8761edd8..e3814d25 100644 --- a/appview/db/pulls.go +++ b/appview/db/pulls.go @@ -433,6 +433,59 @@ func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) { return &pull, nil } +func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { + var pulls []Pull + + rows, err := e.Query(` + select + owner_did, + repo_at, + pull_id, + created, + title, + state + from + pulls + where + owner_did = ? + order by + created desc`, did) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var pull Pull + var createdAt string + err := rows.Scan( + &pull.OwnerDid, + &pull.RepoAt, + &pull.PullId, + &createdAt, + &pull.Title, + &pull.State, + ) + if err != nil { + return nil, err + } + + createdTime, err := time.Parse(time.RFC3339, createdAt) + if err != nil { + return nil, err + } + pull.Created = createdTime + + pulls = append(pulls, pull) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return pulls, nil +} + func NewPullComment(e Execer, comment *PullComment) (int64, error) { query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` res, err := e.Exec( diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 670fe7e1..264b8959 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -168,6 +168,7 @@ type ProfilePageParams struct { FollowStatus db.FollowStatus DidHandleMap map[string]string AvatarUri string + ProfileTimeline []db.ProfileTimelineEvent } type ProfileStats struct { diff --git a/appview/pages/templates/user/profile.html b/appview/pages/templates/user/profile.html index 31ed0161..020de195 100644 --- a/appview/pages/templates/user/profile.html +++ b/appview/pages/templates/user/profile.html @@ -1,23 +1,84 @@ {{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }} {{ define "content" }} -
-
- {{ block "profileCard" . }}{{ end }} -
- -
- {{ block "ownRepos" . }}{{ end }} - {{ block "collaboratingRepos" . }}{{ end }} -
+
+
+ {{ block "profileCard" . }}{{ end }} +
+
+ {{ block "ownRepos" . }}{{ end }} + {{ block "collaboratingRepos" . }}{{ end }} +
+ +
+ {{ block "profileTimeline" . }}{{ end }}
+
+{{ end }} + + + +{{ define "profileTimeline" }} +
+

ACTIVITY

+ {{ range .ProfileTimeline }} + {{ if eq .Type "issue" }} +
+ {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} + {{ $icon := "ban" }} + {{ if .Issue.Open }} + {{ $bgColor = "bg-green-600 dark:bg-green-700" }} + {{ $icon = "circle-dot" }} + {{ end }} +
+ {{ i $icon "w-4 h-4 text-white" }} +
+
+

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

+
+
+ {{ else if eq .Type "pull" }} +
+
+ {{ i "git-pull-request" "w-4 h-4" }} +
+
+

+ {{ .Pull.Title }} #{{ .Pull.PullId }} + on + + {{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }} + +

+
+
+ {{ else if eq .Type "repo" }} +
+
+ {{ i "book-plus" "w-4 h-4" }} +
+
+

+ {{ .Repo.Name }} + +

+
+
+ {{ end }} + {{ end }} +
{{ end }} {{ define "profileCard" }}
{{ if .AvatarUri }} - + {{ end }}

@@ -39,7 +100,7 @@ {{ define "ownRepos" }}

REPOS

-
+
{{ range .Repos }}
This user does not have any repos yet.

{{ end }}
-{{ end }} -{{ define "collaboratingRepos" }}

COLLABORATING ON

-
+
{{ range .CollaboratingRepos }}
This user is not collaborating.

{{ end }}
-{{ end }} +{{ end }} \ No newline at end of file diff --git a/appview/state/profile.go b/appview/state/profile.go new file mode 100644 index 00000000..65a4efc9 --- /dev/null +++ b/appview/state/profile.go @@ -0,0 +1,91 @@ +package state + +import ( + "fmt" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/pages" +) + +func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { + didOrHandle := chi.URLParam(r, "user") + if didOrHandle == "" { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + + ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) + if err != nil { + log.Printf("resolving identity: %s", err) + w.WriteHeader(http.StatusNotFound) + return + } + + repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) + if err != nil { + log.Printf("getting repos for %s: %s", ident.DID.String(), err) + } + + collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) + if err != nil { + log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) + } + + timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) + if err != nil { + log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) + } + + var didsToResolve []string + for _, r := range collaboratingRepos { + didsToResolve = append(didsToResolve, r.Did) + } + for _, evt := range timeline { + didsToResolve = append(didsToResolve, evt.Repo.Did) + } + + resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) + didHandleMap := make(map[string]string) + for _, identity := range resolvedIds { + if !identity.Handle.IsInvalidHandle() { + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) + } else { + didHandleMap[identity.DID.String()] = identity.DID.String() + } + } + + followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) + if err != nil { + log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) + } + + loggedInUser := s.auth.GetUser(r) + followStatus := db.IsNotFollowing + if loggedInUser != nil { + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) + } + + profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) + if err != nil { + log.Println("failed to fetch bsky avatar", err) + } + + s.pages.ProfilePage(w, pages.ProfilePageParams{ + LoggedInUser: loggedInUser, + UserDid: ident.DID.String(), + UserHandle: ident.Handle.String(), + Repos: repos, + CollaboratingRepos: collaboratingRepos, + ProfileStats: pages.ProfileStats{ + Followers: followers, + Following: following, + }, + FollowStatus: db.FollowStatus(followStatus), + DidHandleMap: didHandleMap, + AvatarUri: profileAvatarUri, + ProfileTimeline: timeline, + }) +} diff --git a/appview/state/state.go b/appview/state/state.go index dbb7bda2..6f468880 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -740,75 +740,6 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { } } -func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { - didOrHandle := chi.URLParam(r, "user") - if didOrHandle == "" { - http.Error(w, "Bad request", http.StatusBadRequest) - return - } - - ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) - if err != nil { - log.Printf("resolving identity: %s", err) - w.WriteHeader(http.StatusNotFound) - return - } - - repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) - if err != nil { - log.Printf("getting repos for %s: %s", ident.DID.String(), err) - } - - collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) - if err != nil { - log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) - } - var didsToResolve []string - for _, r := range collaboratingRepos { - didsToResolve = append(didsToResolve, r.Did) - } - resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) - didHandleMap := make(map[string]string) - for _, identity := range resolvedIds { - if !identity.Handle.IsInvalidHandle() { - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) - } else { - didHandleMap[identity.DID.String()] = identity.DID.String() - } - } - - followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) - if err != nil { - log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) - } - - loggedInUser := s.auth.GetUser(r) - followStatus := db.IsNotFollowing - if loggedInUser != nil { - followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) - } - - profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) - if err != nil { - log.Println("failed to fetch bsky avatar", err) - } - - s.pages.ProfilePage(w, pages.ProfilePageParams{ - LoggedInUser: loggedInUser, - UserDid: ident.DID.String(), - UserHandle: ident.Handle.String(), - Repos: repos, - CollaboratingRepos: collaboratingRepos, - ProfileStats: pages.ProfileStats{ - Followers: followers, - Following: following, - }, - FollowStatus: db.FollowStatus(followStatus), - DidHandleMap: didHandleMap, - AvatarUri: profileAvatarUri, - }) -} - func GetAvatarUri(handle string) (string, error) { return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil } -- 2.51.2