-1
appview/db/profile.go
-1
appview/db/profile.go
+139
-27
appview/db/timeline.go
+139
-27
appview/db/timeline.go
···
14
14
15
15
// optional: populate only if Repo is a fork
16
16
Source *Repo
17
+
18
+
// optional: populate only if event is Follow
19
+
*Profile
20
+
*FollowStats
17
21
}
22
+
23
+
type FollowStats struct {
24
+
Followers int
25
+
Following int
26
+
}
27
+
28
+
const Limit = 50
18
29
19
30
// TODO: this gathers heterogenous events from different sources and aggregates
20
31
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
21
32
func MakeTimeline(e Execer) ([]TimelineEvent, error) {
22
33
var events []TimelineEvent
23
-
limit := 50
24
34
25
-
repos, err := GetAllRepos(e, limit)
35
+
repos, err := getTimelineRepos(e)
26
36
if err != nil {
27
37
return nil, err
28
38
}
29
39
30
-
follows, err := GetAllFollows(e, limit)
40
+
stars, err := getTimelineStars(e)
41
+
if err != nil {
42
+
return nil, err
43
+
}
44
+
45
+
follows, err := getTimelineFollows(e)
46
+
if err != nil {
47
+
return nil, err
48
+
}
49
+
50
+
events = append(events, repos...)
51
+
events = append(events, stars...)
52
+
events = append(events, follows...)
53
+
54
+
sort.Slice(events, func(i, j int) bool {
55
+
return events[i].EventAt.After(events[j].EventAt)
56
+
})
57
+
58
+
// Limit the slice to 100 events
59
+
if len(events) > Limit {
60
+
events = events[:Limit]
61
+
}
62
+
63
+
return events, nil
64
+
}
65
+
66
+
func getTimelineRepos(e Execer) ([]TimelineEvent, error) {
67
+
repos, err := GetRepos(e, Limit)
31
68
if err != nil {
32
69
return nil, err
33
70
}
34
71
35
-
stars, err := GetAllStars(e, limit)
72
+
// fetch all source repos
73
+
var args []string
74
+
for _, r := range repos {
75
+
if r.Source != "" {
76
+
args = append(args, r.Source)
77
+
}
78
+
}
79
+
80
+
var origRepos []Repo
81
+
if args != nil {
82
+
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
83
+
}
36
84
if err != nil {
37
85
return nil, err
38
86
}
39
87
40
-
for _, repo := range repos {
41
-
var sourceRepo *Repo
42
-
if repo.Source != "" {
43
-
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
44
-
if err != nil {
45
-
return nil, err
88
+
uriToRepo := make(map[string]Repo)
89
+
for _, r := range origRepos {
90
+
uriToRepo[r.RepoAt().String()] = r
91
+
}
92
+
93
+
var events []TimelineEvent
94
+
for _, r := range repos {
95
+
var source *Repo
96
+
if r.Source != "" {
97
+
if origRepo, ok := uriToRepo[r.Source]; ok {
98
+
source = &origRepo
46
99
}
47
100
}
48
101
49
102
events = append(events, TimelineEvent{
50
-
Repo: &repo,
51
-
EventAt: repo.Created,
52
-
Source: sourceRepo,
103
+
Repo: &r,
104
+
EventAt: r.Created,
105
+
Source: source,
53
106
})
54
107
}
55
108
56
-
for _, follow := range follows {
57
-
events = append(events, TimelineEvent{
58
-
Follow: &follow,
59
-
EventAt: follow.FollowedAt,
60
-
})
109
+
return events, nil
110
+
}
111
+
112
+
func getTimelineStars(e Execer) ([]TimelineEvent, error) {
113
+
stars, err := GetStars(e, Limit)
114
+
if err != nil {
115
+
return nil, err
116
+
}
117
+
118
+
// filter star records without a repo
119
+
n := 0
120
+
for _, s := range stars {
121
+
if s.Repo != nil {
122
+
stars[n] = s
123
+
n++
124
+
}
61
125
}
126
+
stars = stars[:n]
62
127
63
-
for _, star := range stars {
128
+
var events []TimelineEvent
129
+
for _, s := range stars {
64
130
events = append(events, TimelineEvent{
65
-
Star: &star,
66
-
EventAt: star.Created,
131
+
Star: &s,
132
+
EventAt: s.Created,
67
133
})
68
134
}
69
135
70
-
sort.Slice(events, func(i, j int) bool {
71
-
return events[i].EventAt.After(events[j].EventAt)
72
-
})
136
+
return events, nil
137
+
}
138
+
139
+
func getTimelineFollows(e Execer) ([]TimelineEvent, error) {
140
+
follows, err := GetAllFollows(e, Limit)
141
+
if err != nil {
142
+
return nil, err
143
+
}
144
+
145
+
var subjects []string
146
+
for _, f := range follows {
147
+
subjects = append(subjects, f.SubjectDid)
148
+
}
149
+
150
+
if subjects == nil {
151
+
return nil, nil
152
+
}
153
+
154
+
profileMap := make(map[string]Profile)
155
+
profiles, err := GetProfiles(e, FilterIn("did", subjects))
156
+
if err != nil {
157
+
return nil, err
158
+
}
159
+
for _, p := range profiles {
160
+
profileMap[p.Did] = p
161
+
}
162
+
163
+
followStatMap := make(map[string]FollowStats)
164
+
for _, s := range subjects {
165
+
followers, following, err := GetFollowerFollowing(e, s)
166
+
if err != nil {
167
+
return nil, err
168
+
}
169
+
followStatMap[s] = FollowStats{
170
+
Followers: followers,
171
+
Following: following,
172
+
}
173
+
}
73
174
74
-
// Limit the slice to 100 events
75
-
if len(events) > limit {
76
-
events = events[:limit]
175
+
var events []TimelineEvent
176
+
for _, f := range follows {
177
+
profile, ok1 := profileMap[f.SubjectDid]
178
+
followStatMap, ok2 := followStatMap[f.SubjectDid]
179
+
if !ok1 || !ok2 {
180
+
continue
181
+
}
182
+
183
+
events = append(events, TimelineEvent{
184
+
Follow: &f,
185
+
Profile: &profile,
186
+
FollowStats: &followStatMap,
187
+
EventAt: f.FollowedAt,
188
+
})
77
189
}
78
190
79
191
return events, nil
+15
-4
appview/pages/funcmap.go
+15
-4
appview/pages/funcmap.go
···
241
241
return u
242
242
},
243
243
244
-
"tinyAvatar": p.tinyAvatar,
245
-
"langColor": enry.GetColor,
244
+
"tinyAvatar": func(handle string) string {
245
+
return p.avatarUri(handle, "tiny")
246
+
},
247
+
"fullAvatar": func(handle string) string {
248
+
return p.avatarUri(handle, "")
249
+
},
250
+
"langColor": enry.GetColor,
246
251
"layoutSide": func() string {
247
252
return "col-span-1 md:col-span-2 lg:col-span-3"
248
253
},
···
252
257
}
253
258
}
254
259
255
-
func (p *Pages) tinyAvatar(handle string) string {
260
+
func (p *Pages) avatarUri(handle, size string) string {
256
261
handle = strings.TrimPrefix(handle, "@")
262
+
257
263
secret := p.avatar.SharedSecret
258
264
h := hmac.New(sha256.New, []byte(secret))
259
265
h.Write([]byte(handle))
260
266
signature := hex.EncodeToString(h.Sum(nil))
261
-
return fmt.Sprintf("%s/%s/%s?size=tiny", p.avatar.Host, signature, handle)
267
+
268
+
sizeArg := ""
269
+
if size != "" {
270
+
sizeArg = fmt.Sprintf("size=%s", size)
271
+
}
272
+
return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg)
262
273
}
263
274
264
275
func icon(name string, classes []string) (template.HTML, error) {
+102
-75
appview/pages/templates/timeline.html
+102
-75
appview/pages/templates/timeline.html
···
49
49
<p class="text-xl font-bold dark:text-white">Timeline</p>
50
50
</div>
51
51
52
-
<div class="flex flex-col gap-3 relative">
53
-
<div
54
-
class="absolute left-8 top-0 bottom-0 w-px bg-gray-300 dark:bg-gray-600"
55
-
></div>
56
-
{{ range .Timeline }}
57
-
<div
58
-
class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit"
59
-
>
60
-
{{ if .Repo }}
61
-
{{ $userHandle := index $.DidHandleMap .Repo.Did }}
62
-
<div class="flex items-center">
63
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
64
-
{{ template "user/fragments/picHandleLink" $userHandle }}
65
-
{{ if .Source }}
66
-
forked
67
-
<a
68
-
href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}"
69
-
class="no-underline hover:underline"
70
-
>
71
-
{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}</a
72
-
>
73
-
to
74
-
<a
75
-
href="/{{ $userHandle }}/{{ .Repo.Name }}"
76
-
class="no-underline hover:underline"
77
-
>{{ .Repo.Name }}</a
78
-
>
79
-
{{ else }}
80
-
created
81
-
<a
82
-
href="/{{ $userHandle }}/{{ .Repo.Name }}"
83
-
class="no-underline hover:underline"
84
-
>{{ .Repo.Name }}</a
85
-
>
86
-
{{ end }}
87
-
<span
88
-
class="text-gray-700 dark:text-gray-400 text-xs"
89
-
>{{ template "repo/fragments/time" .Repo.Created }}</span
90
-
>
91
-
</p>
92
-
</div>
93
-
{{ else if .Follow }}
94
-
{{ $userHandle := index $.DidHandleMap .Follow.UserDid }}
95
-
{{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }}
96
-
<div class="flex items-center">
97
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
98
-
{{ template "user/fragments/picHandleLink" $userHandle }}
99
-
followed
100
-
{{ template "user/fragments/picHandleLink" $subjectHandle }}
101
-
<span
102
-
class="text-gray-700 dark:text-gray-400 text-xs"
103
-
>{{ template "repo/fragments/time" .Follow.FollowedAt }}</span
104
-
>
105
-
</p>
106
-
</div>
107
-
{{ else if .Star }}
108
-
{{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }}
109
-
{{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }}
110
-
<div class="flex items-center">
111
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
112
-
{{ template "user/fragments/picHandleLink" $starrerHandle }}
113
-
starred
114
-
<a
115
-
href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}"
116
-
class="no-underline hover:underline"
117
-
>{{ $repoOwnerHandle | truncateAt30 }}/{{ .Star.Repo.Name }}</a
118
-
>
119
-
<span
120
-
class="text-gray-700 dark:text-gray-400 text-xs"
121
-
>{{ template "repo/fragments/time" .Star.Created }}</spa
122
-
>
123
-
</p>
124
-
</div>
125
-
{{ end }}
52
+
<div class="flex flex-col gap-4">
53
+
{{ range $i, $e := .Timeline }}
54
+
<div class="relative">
55
+
{{ if ne $i 0 }}
56
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div>
57
+
{{ end }}
58
+
{{ with $e }}
59
+
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
60
+
{{ if .Repo }}
61
+
{{ block "repoEvent" (list $ .Repo .Source) }} {{ end }}
62
+
{{ else if .Star }}
63
+
{{ block "starEvent" (list $ .Star) }} {{ end }}
64
+
{{ else if .Follow }}
65
+
{{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }}
66
+
{{ end }}
126
67
</div>
127
-
{{ end }}
68
+
{{ end }}
69
+
</div>
70
+
{{ end }}
128
71
</div>
129
72
</div>
130
73
{{ end }}
74
+
75
+
{{ define "repoEvent" }}
76
+
{{ $root := index . 0 }}
77
+
{{ $repo := index . 1 }}
78
+
{{ $source := index . 2 }}
79
+
{{ $userHandle := index $root.DidHandleMap $repo.Did }}
80
+
<div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm">
81
+
{{ template "user/fragments/picHandleLink" $userHandle }}
82
+
{{ with $source }}
83
+
forked
84
+
<a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}"class="no-underline hover:underline">
85
+
{{ index $root.DidHandleMap .Did }}/{{ .Name }}
86
+
</a>
87
+
to
88
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a>
89
+
{{ else }}
90
+
created
91
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">
92
+
{{ $repo.Name }}
93
+
</a>
94
+
{{ end }}
95
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span>
96
+
</div>
97
+
{{ with $repo }}
98
+
{{ template "user/fragments/repoCard" (list $root . true) }}
99
+
{{ end }}
100
+
{{ end }}
101
+
102
+
{{ define "starEvent" }}
103
+
{{ $root := index . 0 }}
104
+
{{ $star := index . 1 }}
105
+
{{ with $star }}
106
+
{{ $starrerHandle := index $root.DidHandleMap .StarredByDid }}
107
+
{{ $repoOwnerHandle := index $root.DidHandleMap .Repo.Did }}
108
+
<div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm">
109
+
{{ template "user/fragments/picHandleLink" $starrerHandle }}
110
+
starred
111
+
<a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">
112
+
{{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }}
113
+
</a>
114
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span>
115
+
</div>
116
+
{{ with .Repo }}
117
+
{{ template "user/fragments/repoCard" (list $root . true) }}
118
+
{{ end }}
119
+
{{ end }}
120
+
{{ end }}
121
+
122
+
123
+
{{ define "followEvent" }}
124
+
{{ $root := index . 0 }}
125
+
{{ $follow := index . 1 }}
126
+
{{ $profile := index . 2 }}
127
+
{{ $stat := index . 3 }}
128
+
129
+
{{ $userHandle := index $root.DidHandleMap $follow.UserDid }}
130
+
{{ $subjectHandle := index $root.DidHandleMap $follow.SubjectDid }}
131
+
<div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm">
132
+
{{ template "user/fragments/picHandleLink" $userHandle }}
133
+
followed
134
+
{{ template "user/fragments/picHandleLink" $subjectHandle }}
135
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span>
136
+
</div>
137
+
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
138
+
<div class="flex-shrink-0 max-h-full w-24 h-24">
139
+
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" />
140
+
</div>
141
+
142
+
<div class="flex-1 min-h-0 justify-around flex flex-col">
143
+
<a href="/{{ $subjectHandle }}">
144
+
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span>
145
+
</a>
146
+
{{ with $profile.Description }}
147
+
<p class="text-sm pb-2 md:pb-2">{{.}}</p>
148
+
{{ end }}
149
+
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
150
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
151
+
<span id="followers">{{ $stat.Followers }} followers</span>
152
+
<span class="select-none after:content-['·']"></span>
153
+
<span id="following">{{ $stat.Following }} following</span>
154
+
</div>
155
+
</div>
156
+
</div>
157
+
{{ end }}
+27
-27
appview/pages/templates/user/fragments/repoCard.html
+27
-27
appview/pages/templates/user/fragments/repoCard.html
···
5
5
6
6
{{ with $repo }}
7
7
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
8
-
<div class="font-medium dark:text-white">
8
+
<div class="font-medium dark:text-white flex gap-2 items-center">
9
9
{{- if $fullName -}}
10
10
<a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}">{{ index $root.DidHandleMap .Did }}/{{ .Name }}</a>
11
11
{{- else -}}
···
26
26
{{ end }}
27
27
28
28
{{ define "repoStats" }}
29
-
<div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-4 mt-auto">
30
-
{{ with .Language }}
31
-
<div class="flex gap-2 items-center text-sm">
32
-
<div class="size-2 rounded-full" style="background-color: {{ langColor . }};"></div>
33
-
<span>{{ . }}</span>
34
-
</div>
35
-
{{ end }}
36
-
{{ with .StarCount }}
37
-
<div class="flex gap-1 items-center text-sm">
38
-
{{ i "star" "w-3 h-3 fill-current" }}
39
-
<span>{{ . }}</span>
40
-
</div>
41
-
{{ end }}
42
-
{{ with .IssueCount.Open }}
43
-
<div class="flex gap-1 items-center text-sm">
44
-
{{ i "circle-dot" "w-3 h-3" }}
45
-
<span>{{ . }}</span>
46
-
</div>
47
-
{{ end }}
48
-
{{ with .PullCount.Open }}
49
-
<div class="flex gap-1 items-center text-sm">
50
-
{{ i "git-pull-request" "w-3 h-3" }}
51
-
<span>{{ . }}</span>
52
-
</div>
53
-
{{ end }}
54
-
</div>
29
+
<div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-4 mt-auto">
30
+
{{ with .Language }}
31
+
<div class="flex gap-2 items-center text-sm">
32
+
<div class="size-2 rounded-full" style="background-color: {{ langColor . }};"></div>
33
+
<span>{{ . }}</span>
34
+
</div>
35
+
{{ end }}
36
+
{{ with .StarCount }}
37
+
<div class="flex gap-1 items-center text-sm">
38
+
{{ i "star" "w-3 h-3 fill-current" }}
39
+
<span>{{ . }}</span>
40
+
</div>
41
+
{{ end }}
42
+
{{ with .IssueCount.Open }}
43
+
<div class="flex gap-1 items-center text-sm">
44
+
{{ i "circle-dot" "w-3 h-3" }}
45
+
<span>{{ . }}</span>
46
+
</div>
47
+
{{ end }}
48
+
{{ with .PullCount.Open }}
49
+
<div class="flex gap-1 items-center text-sm">
50
+
{{ i "git-pull-request" "w-3 h-3" }}
51
+
<span>{{ . }}</span>
52
+
</div>
53
+
{{ end }}
54
+
</div>
55
55
{{ end }}
56
56
57
57
+5
-5
appview/pages/templates/user/profile.html
+5
-5
appview/pages/templates/user/profile.html
···
8
8
{{ end }}
9
9
10
10
{{ define "content" }}
11
-
<div class="grid grid-cols-1 md:grid-cols-8 gap-4">
12
-
<div class="md:col-span-2 order-1 md:order-1">
11
+
<div class="grid grid-cols-1 md:grid-cols-11 gap-4">
12
+
<div class="md:col-span-3 order-1 md:order-1">
13
13
<div class="grid grid-cols-1 gap-4">
14
14
{{ template "user/fragments/profileCard" .Card }}
15
15
{{ block "punchcard" .Punchcard }} {{ end }}
16
16
</div>
17
17
</div>
18
-
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
18
+
<div id="all-repos" class="md:col-span-4 order-2 md:order-2">
19
19
<div class="grid grid-cols-1 gap-4">
20
20
{{ block "ownRepos" . }}{{ end }}
21
21
{{ block "collaboratingRepos" . }}{{ end }}
22
22
</div>
23
23
</div>
24
-
<div class="md:col-span-3 order-3 md:order-3">
24
+
<div class="md:col-span-4 order-3 md:order-3">
25
25
{{ block "profileTimeline" . }}{{ end }}
26
26
</div>
27
27
</div>
···
258
258
</button>
259
259
{{ end }}
260
260
</div>
261
-
<div id="repos" class="grid grid-cols-1 gap-4">
261
+
<div id="repos" class="grid grid-cols-1 gap-4 items-stretch">
262
262
{{ range .Repos }}
263
263
{{ template "user/fragments/repoCard" (list $ . false) }}
264
264
{{ else }}