+60
-5
appview/db/issues.go
+60
-5
appview/db/issues.go
···
118
118
issues i
119
119
left join
120
120
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
121
-
where
121
+
where
122
122
i.repo_at = ? and i.open = ?
123
123
group by
124
124
i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open
···
156
156
return issues, nil
157
157
}
158
158
159
+
func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) {
160
+
var issues []Issue
161
+
162
+
rows, err := e.Query(
163
+
`select
164
+
i.owner_did,
165
+
i.repo_at,
166
+
i.issue_id,
167
+
i.created,
168
+
i.title,
169
+
i.body,
170
+
i.open,
171
+
count(c.id)
172
+
from
173
+
issues i
174
+
left join
175
+
comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id
176
+
where
177
+
i.owner_did = ?
178
+
group by
179
+
i.id, i.owner_did, i.repo_at, i.issue_id, i.created, i.title, i.body, i.open
180
+
order by
181
+
i.created desc`,
182
+
ownerDid)
183
+
if err != nil {
184
+
return nil, err
185
+
}
186
+
defer rows.Close()
187
+
188
+
for rows.Next() {
189
+
var issue Issue
190
+
var createdAt string
191
+
var metadata IssueMetadata
192
+
err := rows.Scan(&issue.OwnerDid, &issue.RepoAt, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
193
+
if err != nil {
194
+
return nil, err
195
+
}
196
+
197
+
createdTime, err := time.Parse(time.RFC3339, createdAt)
198
+
if err != nil {
199
+
return nil, err
200
+
}
201
+
issue.Created = &createdTime
202
+
issue.Metadata = &metadata
203
+
204
+
issues = append(issues, issue)
205
+
}
206
+
207
+
if err := rows.Err(); err != nil {
208
+
return nil, err
209
+
}
210
+
211
+
return issues, nil
212
+
}
213
+
159
214
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
160
215
query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
161
216
row := e.QueryRow(query, repoAt, issueId)
···
219
274
var comments []Comment
220
275
221
276
rows, err := e.Query(`
222
-
select
277
+
select
223
278
owner_did,
224
279
issue_id,
225
280
comment_id,
···
230
285
deleted
231
286
from
232
287
comments
233
-
where
234
-
repo_at = ? and issue_id = ?
288
+
where
289
+
repo_at = ? and issue_id = ?
235
290
order by
236
291
created asc`,
237
292
repoAt,
···
354
409
func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error {
355
410
_, err := e.Exec(
356
411
`
357
-
update comments
412
+
update comments
358
413
set body = "",
359
414
deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
360
415
where repo_at = ? and issue_id = ? and comment_id = ?
+75
appview/db/profile.go
+75
appview/db/profile.go
···
1
+
package db
2
+
3
+
import (
4
+
"sort"
5
+
"time"
6
+
)
7
+
8
+
type ProfileTimelineEvent struct {
9
+
EventAt time.Time
10
+
Type string
11
+
*Issue
12
+
*Pull
13
+
*Repo
14
+
}
15
+
16
+
func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) {
17
+
timeline := []ProfileTimelineEvent{}
18
+
19
+
pulls, err := GetPullsByOwnerDid(e, forDid)
20
+
if err != nil {
21
+
return timeline, err
22
+
}
23
+
24
+
for _, pull := range pulls {
25
+
repo, err := GetRepoByAtUri(e, string(pull.RepoAt))
26
+
if err != nil {
27
+
return timeline, err
28
+
}
29
+
30
+
timeline = append(timeline, ProfileTimelineEvent{
31
+
EventAt: pull.Created,
32
+
Type: "pull",
33
+
Pull: &pull,
34
+
Repo: repo,
35
+
})
36
+
}
37
+
38
+
issues, err := GetIssuesByOwnerDid(e, forDid)
39
+
if err != nil {
40
+
return timeline, err
41
+
}
42
+
43
+
for _, issue := range issues {
44
+
repo, err := GetRepoByAtUri(e, string(issue.RepoAt))
45
+
if err != nil {
46
+
return timeline, err
47
+
}
48
+
49
+
timeline = append(timeline, ProfileTimelineEvent{
50
+
EventAt: *issue.Created,
51
+
Type: "issue",
52
+
Issue: &issue,
53
+
Repo: repo,
54
+
})
55
+
}
56
+
57
+
repos, err := GetAllReposByDid(e, forDid)
58
+
if err != nil {
59
+
return timeline, err
60
+
}
61
+
62
+
for _, repo := range repos {
63
+
timeline = append(timeline, ProfileTimelineEvent{
64
+
EventAt: repo.Created,
65
+
Type: "repo",
66
+
Repo: &repo,
67
+
})
68
+
}
69
+
70
+
sort.Slice(timeline, func(i, j int) bool {
71
+
return timeline[i].EventAt.After(timeline[j].EventAt)
72
+
})
73
+
74
+
return timeline, nil
75
+
}
+53
appview/db/pulls.go
+53
appview/db/pulls.go
···
433
433
return &pull, nil
434
434
}
435
435
436
+
func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) {
437
+
var pulls []Pull
438
+
439
+
rows, err := e.Query(`
440
+
select
441
+
owner_did,
442
+
repo_at,
443
+
pull_id,
444
+
created,
445
+
title,
446
+
state
447
+
from
448
+
pulls
449
+
where
450
+
owner_did = ?
451
+
order by
452
+
created desc`, did)
453
+
if err != nil {
454
+
return nil, err
455
+
}
456
+
defer rows.Close()
457
+
458
+
for rows.Next() {
459
+
var pull Pull
460
+
var createdAt string
461
+
err := rows.Scan(
462
+
&pull.OwnerDid,
463
+
&pull.RepoAt,
464
+
&pull.PullId,
465
+
&createdAt,
466
+
&pull.Title,
467
+
&pull.State,
468
+
)
469
+
if err != nil {
470
+
return nil, err
471
+
}
472
+
473
+
createdTime, err := time.Parse(time.RFC3339, createdAt)
474
+
if err != nil {
475
+
return nil, err
476
+
}
477
+
pull.Created = createdTime
478
+
479
+
pulls = append(pulls, pull)
480
+
}
481
+
482
+
if err := rows.Err(); err != nil {
483
+
return nil, err
484
+
}
485
+
486
+
return pulls, nil
487
+
}
488
+
436
489
func NewPullComment(e Execer, comment *PullComment) (int64, error) {
437
490
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
438
491
res, err := e.Exec(
+1
appview/pages/pages.go
+1
appview/pages/pages.go
+74
-15
appview/pages/templates/user/profile.html
+74
-15
appview/pages/templates/user/profile.html
···
1
1
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
5
-
<div class="md:col-span-1">
6
-
{{ block "profileCard" . }}{{ end }}
7
-
</div>
4
+
<div class="grid grid-cols-1 md:grid-cols-5 gap-6">
5
+
<div class="md:col-span-1 order-1 md:order-1">
6
+
{{ block "profileCard" . }}{{ end }}
7
+
</div>
8
+
<div class="md:col-span-2 order-2 md:order-2">
9
+
{{ block "ownRepos" . }}{{ end }}
10
+
{{ block "collaboratingRepos" . }}{{ end }}
11
+
</div>
12
+
13
+
<div class="md:col-span-2 order-3 md:order-3">
14
+
{{ block "profileTimeline" . }}{{ end }}
15
+
</div>
16
+
</div>
17
+
{{ end }}
18
+
19
+
8
20
9
-
<div class="md:col-span-3">
10
-
{{ block "ownRepos" . }}{{ end }}
11
-
{{ block "collaboratingRepos" . }}{{ end }}
12
-
</div>
13
-
</div>
21
+
{{ define "profileTimeline" }}
22
+
<div class="flex flex-col gap-3 relative">
23
+
<p class="px-6 text-sm font-bold py-2 dark:text-white">ACTIVITY</p>
24
+
{{ range .ProfileTimeline }}
25
+
{{ if eq .Type "issue" }}
26
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit max-w-full flex items-center gap-2">
27
+
{{ $bgColor := "bg-gray-800 dark:bg-gray-700" }}
28
+
{{ $icon := "ban" }}
29
+
{{ if .Issue.Open }}
30
+
{{ $bgColor = "bg-green-600 dark:bg-green-700" }}
31
+
{{ $icon = "circle-dot" }}
32
+
{{ end }}
33
+
<div class="{{ $bgColor }} text-white rounded-full p-1">
34
+
{{ i $icon "w-4 h-4 text-white" }}
35
+
</div>
36
+
<div>
37
+
<p class="text-gray-600 dark:text-gray-300">
38
+
<a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .Issue.IssueId }}" class="no-underline hover:underline">{{ .Issue.Title }} <span class="text-gray-500 dark:text-gray-400">#{{ .Issue.IssueId }}</span></a>
39
+
on
40
+
<a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ index $.DidHandleMap .Repo.Did }}<span class="select-none">/</span>{{ .Repo.Name }}</a>
41
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time>
42
+
</p>
43
+
</div>
44
+
</div>
45
+
{{ else if eq .Type "pull" }}
46
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit flex items-center gap-3">
47
+
<div class="bg-purple-600 dark:bg-purple-700 text-white rounded-full p-1">
48
+
{{ i "git-pull-request" "w-4 h-4" }}
49
+
</div>
50
+
<div>
51
+
<p class="text-gray-600 dark:text-gray-300">
52
+
<a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}/pulls/{{ .Pull.PullId }}" class="no-underline hover:underline">{{ .Pull.Title }} <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span></a>
53
+
on
54
+
<a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">
55
+
{{ index $.DidHandleMap .Repo.Did }}<span class="select-none">/</span>{{ .Repo.Name }}</a>
56
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time>
57
+
</p>
58
+
</div>
59
+
</div>
60
+
{{ else if eq .Type "repo" }}
61
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit flex items-center gap-3">
62
+
<div class="bg-gray-200 dark:bg-gray-300 text-black rounded-full p-1">
63
+
{{ i "book-plus" "w-4 h-4" }}
64
+
</div>
65
+
<div>
66
+
<p class="text-gray-600 dark:text-gray-300">
67
+
<a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a>
68
+
<time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time>
69
+
</p>
70
+
</div>
71
+
</div>
72
+
{{ end }}
73
+
{{ end }}
74
+
</div>
14
75
{{ end }}
15
76
16
77
{{ define "profileCard" }}
17
78
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit">
18
79
<div class="flex justify-center items-center">
19
80
{{ if .AvatarUri }}
20
-
<img class="w-1/2 rounded-full p-2" src="{{ .AvatarUri }}" />
81
+
<img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" />
21
82
{{ end }}
22
83
</div>
23
84
<p class="text-xl font-bold text-center dark:text-white">
···
39
100
40
101
{{ define "ownRepos" }}
41
102
<p class="text-sm font-bold py-2 px-6 dark:text-white">REPOS</p>
42
-
<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
103
+
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
43
104
{{ range .Repos }}
44
105
<div
45
106
id="repo-card"
···
71
132
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
72
133
{{ end }}
73
134
</div>
74
-
{{ end }}
75
135
76
-
{{ define "collaboratingRepos" }}
77
136
<p class="text-sm font-bold py-2 px-6 dark:text-white">COLLABORATING ON</p>
78
-
<div id="collaborating" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
137
+
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
79
138
{{ range .CollaboratingRepos }}
80
139
<div
81
140
id="repo-card"
···
105
164
<p class="px-6 dark:text-white">This user is not collaborating.</p>
106
165
{{ end }}
107
166
</div>
108
-
{{ end }}
167
+
{{ end }}
+91
appview/state/profile.go
+91
appview/state/profile.go
···
1
+
package state
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
8
+
"github.com/go-chi/chi/v5"
9
+
"tangled.sh/tangled.sh/core/appview/db"
10
+
"tangled.sh/tangled.sh/core/appview/pages"
11
+
)
12
+
13
+
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
14
+
didOrHandle := chi.URLParam(r, "user")
15
+
if didOrHandle == "" {
16
+
http.Error(w, "Bad request", http.StatusBadRequest)
17
+
return
18
+
}
19
+
20
+
ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
21
+
if err != nil {
22
+
log.Printf("resolving identity: %s", err)
23
+
w.WriteHeader(http.StatusNotFound)
24
+
return
25
+
}
26
+
27
+
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
28
+
if err != nil {
29
+
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
30
+
}
31
+
32
+
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
33
+
if err != nil {
34
+
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
35
+
}
36
+
37
+
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
38
+
if err != nil {
39
+
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
40
+
}
41
+
42
+
var didsToResolve []string
43
+
for _, r := range collaboratingRepos {
44
+
didsToResolve = append(didsToResolve, r.Did)
45
+
}
46
+
for _, evt := range timeline {
47
+
didsToResolve = append(didsToResolve, evt.Repo.Did)
48
+
}
49
+
50
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
51
+
didHandleMap := make(map[string]string)
52
+
for _, identity := range resolvedIds {
53
+
if !identity.Handle.IsInvalidHandle() {
54
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
55
+
} else {
56
+
didHandleMap[identity.DID.String()] = identity.DID.String()
57
+
}
58
+
}
59
+
60
+
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
61
+
if err != nil {
62
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
63
+
}
64
+
65
+
loggedInUser := s.auth.GetUser(r)
66
+
followStatus := db.IsNotFollowing
67
+
if loggedInUser != nil {
68
+
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
69
+
}
70
+
71
+
profileAvatarUri, err := GetAvatarUri(ident.Handle.String())
72
+
if err != nil {
73
+
log.Println("failed to fetch bsky avatar", err)
74
+
}
75
+
76
+
s.pages.ProfilePage(w, pages.ProfilePageParams{
77
+
LoggedInUser: loggedInUser,
78
+
UserDid: ident.DID.String(),
79
+
UserHandle: ident.Handle.String(),
80
+
Repos: repos,
81
+
CollaboratingRepos: collaboratingRepos,
82
+
ProfileStats: pages.ProfileStats{
83
+
Followers: followers,
84
+
Following: following,
85
+
},
86
+
FollowStatus: db.FollowStatus(followStatus),
87
+
DidHandleMap: didHandleMap,
88
+
AvatarUri: profileAvatarUri,
89
+
ProfileTimeline: timeline,
90
+
})
91
+
}
-69
appview/state/state.go
-69
appview/state/state.go
···
740
740
}
741
741
}
742
742
743
-
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
744
-
didOrHandle := chi.URLParam(r, "user")
745
-
if didOrHandle == "" {
746
-
http.Error(w, "Bad request", http.StatusBadRequest)
747
-
return
748
-
}
749
-
750
-
ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
751
-
if err != nil {
752
-
log.Printf("resolving identity: %s", err)
753
-
w.WriteHeader(http.StatusNotFound)
754
-
return
755
-
}
756
-
757
-
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
758
-
if err != nil {
759
-
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
760
-
}
761
-
762
-
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
763
-
if err != nil {
764
-
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
765
-
}
766
-
var didsToResolve []string
767
-
for _, r := range collaboratingRepos {
768
-
didsToResolve = append(didsToResolve, r.Did)
769
-
}
770
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
771
-
didHandleMap := make(map[string]string)
772
-
for _, identity := range resolvedIds {
773
-
if !identity.Handle.IsInvalidHandle() {
774
-
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
775
-
} else {
776
-
didHandleMap[identity.DID.String()] = identity.DID.String()
777
-
}
778
-
}
779
-
780
-
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
781
-
if err != nil {
782
-
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
783
-
}
784
-
785
-
loggedInUser := s.auth.GetUser(r)
786
-
followStatus := db.IsNotFollowing
787
-
if loggedInUser != nil {
788
-
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
789
-
}
790
-
791
-
profileAvatarUri, err := GetAvatarUri(ident.Handle.String())
792
-
if err != nil {
793
-
log.Println("failed to fetch bsky avatar", err)
794
-
}
795
-
796
-
s.pages.ProfilePage(w, pages.ProfilePageParams{
797
-
LoggedInUser: loggedInUser,
798
-
UserDid: ident.DID.String(),
799
-
UserHandle: ident.Handle.String(),
800
-
Repos: repos,
801
-
CollaboratingRepos: collaboratingRepos,
802
-
ProfileStats: pages.ProfileStats{
803
-
Followers: followers,
804
-
Following: following,
805
-
},
806
-
FollowStatus: db.FollowStatus(followStatus),
807
-
DidHandleMap: didHandleMap,
808
-
AvatarUri: profileAvatarUri,
809
-
})
810
-
}
811
-
812
743
func GetAvatarUri(handle string) (string, error) {
813
744
return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil
814
745
}