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

appview/db: rework timeline to use card-style interface

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 75a13776 444b9204

verified
Changed files
+288 -139
appview
-1
appview/db/profile.go
···
}
idx := idxs[did]
-
log.Println("idx", "idx", idx, "link", link)
profileMap[did].Links[idx] = link
idxs[did] = idx + 1
}
···
}
idx := idxs[did]
profileMap[did].Links[idx] = link
idxs[did] = idx + 1
}
+139 -27
appview/db/timeline.go
···
// optional: populate only if Repo is a fork
Source *Repo
}
// TODO: this gathers heterogenous events from different sources and aggregates
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
func MakeTimeline(e Execer) ([]TimelineEvent, error) {
var events []TimelineEvent
-
limit := 50
-
repos, err := GetAllRepos(e, limit)
if err != nil {
return nil, err
}
-
follows, err := GetAllFollows(e, limit)
if err != nil {
return nil, err
}
-
stars, err := GetAllStars(e, limit)
if err != nil {
return nil, err
}
-
for _, repo := range repos {
-
var sourceRepo *Repo
-
if repo.Source != "" {
-
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
-
if err != nil {
-
return nil, err
}
}
events = append(events, TimelineEvent{
-
Repo: &repo,
-
EventAt: repo.Created,
-
Source: sourceRepo,
})
}
-
for _, follow := range follows {
-
events = append(events, TimelineEvent{
-
Follow: &follow,
-
EventAt: follow.FollowedAt,
-
})
}
-
for _, star := range stars {
events = append(events, TimelineEvent{
-
Star: &star,
-
EventAt: star.Created,
})
}
-
sort.Slice(events, func(i, j int) bool {
-
return events[i].EventAt.After(events[j].EventAt)
-
})
-
// Limit the slice to 100 events
-
if len(events) > limit {
-
events = events[:limit]
}
return events, nil
···
// optional: populate only if Repo is a fork
Source *Repo
+
+
// optional: populate only if event is Follow
+
*Profile
+
*FollowStats
}
+
+
type FollowStats struct {
+
Followers int
+
Following int
+
}
+
+
const Limit = 50
// TODO: this gathers heterogenous events from different sources and aggregates
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
func MakeTimeline(e Execer) ([]TimelineEvent, error) {
var events []TimelineEvent
+
repos, err := getTimelineRepos(e)
if err != nil {
return nil, err
}
+
stars, err := getTimelineStars(e)
+
if err != nil {
+
return nil, err
+
}
+
+
follows, err := getTimelineFollows(e)
+
if err != nil {
+
return nil, err
+
}
+
+
events = append(events, repos...)
+
events = append(events, stars...)
+
events = append(events, follows...)
+
+
sort.Slice(events, func(i, j int) bool {
+
return events[i].EventAt.After(events[j].EventAt)
+
})
+
+
// Limit the slice to 100 events
+
if len(events) > Limit {
+
events = events[:Limit]
+
}
+
+
return events, nil
+
}
+
+
func getTimelineRepos(e Execer) ([]TimelineEvent, error) {
+
repos, err := GetRepos(e, Limit)
if err != nil {
return nil, err
}
+
// fetch all source repos
+
var args []string
+
for _, r := range repos {
+
if r.Source != "" {
+
args = append(args, r.Source)
+
}
+
}
+
+
var origRepos []Repo
+
if args != nil {
+
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
+
}
if err != nil {
return nil, err
}
+
uriToRepo := make(map[string]Repo)
+
for _, r := range origRepos {
+
uriToRepo[r.RepoAt().String()] = r
+
}
+
+
var events []TimelineEvent
+
for _, r := range repos {
+
var source *Repo
+
if r.Source != "" {
+
if origRepo, ok := uriToRepo[r.Source]; ok {
+
source = &origRepo
}
}
events = append(events, TimelineEvent{
+
Repo: &r,
+
EventAt: r.Created,
+
Source: source,
})
}
+
return events, nil
+
}
+
+
func getTimelineStars(e Execer) ([]TimelineEvent, error) {
+
stars, err := GetStars(e, Limit)
+
if err != nil {
+
return nil, err
+
}
+
+
// filter star records without a repo
+
n := 0
+
for _, s := range stars {
+
if s.Repo != nil {
+
stars[n] = s
+
n++
+
}
}
+
stars = stars[:n]
+
var events []TimelineEvent
+
for _, s := range stars {
events = append(events, TimelineEvent{
+
Star: &s,
+
EventAt: s.Created,
})
}
+
return events, nil
+
}
+
+
func getTimelineFollows(e Execer) ([]TimelineEvent, error) {
+
follows, err := GetAllFollows(e, Limit)
+
if err != nil {
+
return nil, err
+
}
+
+
var subjects []string
+
for _, f := range follows {
+
subjects = append(subjects, f.SubjectDid)
+
}
+
+
if subjects == nil {
+
return nil, nil
+
}
+
+
profileMap := make(map[string]Profile)
+
profiles, err := GetProfiles(e, FilterIn("did", subjects))
+
if err != nil {
+
return nil, err
+
}
+
for _, p := range profiles {
+
profileMap[p.Did] = p
+
}
+
+
followStatMap := make(map[string]FollowStats)
+
for _, s := range subjects {
+
followers, following, err := GetFollowerFollowing(e, s)
+
if err != nil {
+
return nil, err
+
}
+
followStatMap[s] = FollowStats{
+
Followers: followers,
+
Following: following,
+
}
+
}
+
var events []TimelineEvent
+
for _, f := range follows {
+
profile, ok1 := profileMap[f.SubjectDid]
+
followStatMap, ok2 := followStatMap[f.SubjectDid]
+
if !ok1 || !ok2 {
+
continue
+
}
+
+
events = append(events, TimelineEvent{
+
Follow: &f,
+
Profile: &profile,
+
FollowStats: &followStatMap,
+
EventAt: f.FollowedAt,
+
})
}
return events, nil
+15 -4
appview/pages/funcmap.go
···
return u
},
-
"tinyAvatar": p.tinyAvatar,
-
"langColor": enry.GetColor,
"layoutSide": func() string {
return "col-span-1 md:col-span-2 lg:col-span-3"
},
···
}
}
-
func (p *Pages) tinyAvatar(handle string) string {
handle = strings.TrimPrefix(handle, "@")
secret := p.avatar.SharedSecret
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(handle))
signature := hex.EncodeToString(h.Sum(nil))
-
return fmt.Sprintf("%s/%s/%s?size=tiny", p.avatar.Host, signature, handle)
}
func icon(name string, classes []string) (template.HTML, error) {
···
return u
},
+
"tinyAvatar": func(handle string) string {
+
return p.avatarUri(handle, "tiny")
+
},
+
"fullAvatar": func(handle string) string {
+
return p.avatarUri(handle, "")
+
},
+
"langColor": enry.GetColor,
"layoutSide": func() string {
return "col-span-1 md:col-span-2 lg:col-span-3"
},
···
}
}
+
func (p *Pages) avatarUri(handle, size string) string {
handle = strings.TrimPrefix(handle, "@")
+
secret := p.avatar.SharedSecret
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(handle))
signature := hex.EncodeToString(h.Sum(nil))
+
+
sizeArg := ""
+
if size != "" {
+
sizeArg = fmt.Sprintf("size=%s", size)
+
}
+
return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg)
}
func icon(name string, classes []string) (template.HTML, error) {
+102 -75
appview/pages/templates/timeline.html
···
<p class="text-xl font-bold dark:text-white">Timeline</p>
</div>
-
<div class="flex flex-col gap-3 relative">
-
<div
-
class="absolute left-8 top-0 bottom-0 w-px bg-gray-300 dark:bg-gray-600"
-
></div>
-
{{ range .Timeline }}
-
<div
-
class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit"
-
>
-
{{ if .Repo }}
-
{{ $userHandle := index $.DidHandleMap .Repo.Did }}
-
<div class="flex items-center">
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
-
{{ template "user/fragments/picHandleLink" $userHandle }}
-
{{ if .Source }}
-
forked
-
<a
-
href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}"
-
class="no-underline hover:underline"
-
>
-
{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}</a
-
>
-
to
-
<a
-
href="/{{ $userHandle }}/{{ .Repo.Name }}"
-
class="no-underline hover:underline"
-
>{{ .Repo.Name }}</a
-
>
-
{{ else }}
-
created
-
<a
-
href="/{{ $userHandle }}/{{ .Repo.Name }}"
-
class="no-underline hover:underline"
-
>{{ .Repo.Name }}</a
-
>
-
{{ end }}
-
<span
-
class="text-gray-700 dark:text-gray-400 text-xs"
-
>{{ template "repo/fragments/time" .Repo.Created }}</span
-
>
-
</p>
-
</div>
-
{{ else if .Follow }}
-
{{ $userHandle := index $.DidHandleMap .Follow.UserDid }}
-
{{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }}
-
<div class="flex items-center">
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
-
{{ template "user/fragments/picHandleLink" $userHandle }}
-
followed
-
{{ template "user/fragments/picHandleLink" $subjectHandle }}
-
<span
-
class="text-gray-700 dark:text-gray-400 text-xs"
-
>{{ template "repo/fragments/time" .Follow.FollowedAt }}</span
-
>
-
</p>
-
</div>
-
{{ else if .Star }}
-
{{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }}
-
{{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }}
-
<div class="flex items-center">
-
<p class="text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2">
-
{{ template "user/fragments/picHandleLink" $starrerHandle }}
-
starred
-
<a
-
href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}"
-
class="no-underline hover:underline"
-
>{{ $repoOwnerHandle | truncateAt30 }}/{{ .Star.Repo.Name }}</a
-
>
-
<span
-
class="text-gray-700 dark:text-gray-400 text-xs"
-
>{{ template "repo/fragments/time" .Star.Created }}</spa
-
>
-
</p>
-
</div>
-
{{ end }}
</div>
-
{{ end }}
</div>
</div>
{{ end }}
···
<p class="text-xl font-bold dark:text-white">Timeline</p>
</div>
+
<div class="flex flex-col gap-4">
+
{{ range $i, $e := .Timeline }}
+
<div class="relative">
+
{{ if ne $i 0 }}
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div>
+
{{ end }}
+
{{ with $e }}
+
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
+
{{ if .Repo }}
+
{{ block "repoEvent" (list $ .Repo .Source) }} {{ end }}
+
{{ else if .Star }}
+
{{ block "starEvent" (list $ .Star) }} {{ end }}
+
{{ else if .Follow }}
+
{{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }}
+
{{ end }}
</div>
+
{{ end }}
+
</div>
+
{{ end }}
</div>
</div>
{{ end }}
+
+
{{ define "repoEvent" }}
+
{{ $root := index . 0 }}
+
{{ $repo := index . 1 }}
+
{{ $source := index . 2 }}
+
{{ $userHandle := index $root.DidHandleMap $repo.Did }}
+
<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">
+
{{ template "user/fragments/picHandleLink" $userHandle }}
+
{{ with $source }}
+
forked
+
<a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}"class="no-underline hover:underline">
+
{{ index $root.DidHandleMap .Did }}/{{ .Name }}
+
</a>
+
to
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a>
+
{{ else }}
+
created
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">
+
{{ $repo.Name }}
+
</a>
+
{{ end }}
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span>
+
</div>
+
{{ with $repo }}
+
{{ template "user/fragments/repoCard" (list $root . true) }}
+
{{ end }}
+
{{ end }}
+
+
{{ define "starEvent" }}
+
{{ $root := index . 0 }}
+
{{ $star := index . 1 }}
+
{{ with $star }}
+
{{ $starrerHandle := index $root.DidHandleMap .StarredByDid }}
+
{{ $repoOwnerHandle := index $root.DidHandleMap .Repo.Did }}
+
<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">
+
{{ template "user/fragments/picHandleLink" $starrerHandle }}
+
starred
+
<a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">
+
{{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }}
+
</a>
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span>
+
</div>
+
{{ with .Repo }}
+
{{ template "user/fragments/repoCard" (list $root . true) }}
+
{{ end }}
+
{{ end }}
+
{{ end }}
+
+
+
{{ define "followEvent" }}
+
{{ $root := index . 0 }}
+
{{ $follow := index . 1 }}
+
{{ $profile := index . 2 }}
+
{{ $stat := index . 3 }}
+
+
{{ $userHandle := index $root.DidHandleMap $follow.UserDid }}
+
{{ $subjectHandle := index $root.DidHandleMap $follow.SubjectDid }}
+
<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">
+
{{ template "user/fragments/picHandleLink" $userHandle }}
+
followed
+
{{ template "user/fragments/picHandleLink" $subjectHandle }}
+
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span>
+
</div>
+
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4">
+
<div class="flex-shrink-0 max-h-full w-24 h-24">
+
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" />
+
</div>
+
+
<div class="flex-1 min-h-0 justify-around flex flex-col">
+
<a href="/{{ $subjectHandle }}">
+
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span>
+
</a>
+
{{ with $profile.Description }}
+
<p class="text-sm pb-2 md:pb-2">{{.}}</p>
+
{{ end }}
+
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm">
+
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+
<span id="followers">{{ $stat.Followers }} followers</span>
+
<span class="select-none after:content-['·']"></span>
+
<span id="following">{{ $stat.Following }} following</span>
+
</div>
+
</div>
+
</div>
+
{{ end }}
+27 -27
appview/pages/templates/user/fragments/repoCard.html
···
{{ with $repo }}
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
-
<div class="font-medium dark:text-white">
{{- if $fullName -}}
<a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}">{{ index $root.DidHandleMap .Did }}/{{ .Name }}</a>
{{- else -}}
···
{{ end }}
{{ define "repoStats" }}
-
<div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-4 mt-auto">
-
{{ with .Language }}
-
<div class="flex gap-2 items-center text-sm">
-
<div class="size-2 rounded-full" style="background-color: {{ langColor . }};"></div>
-
<span>{{ . }}</span>
-
</div>
-
{{ end }}
-
{{ with .StarCount }}
-
<div class="flex gap-1 items-center text-sm">
-
{{ i "star" "w-3 h-3 fill-current" }}
-
<span>{{ . }}</span>
-
</div>
-
{{ end }}
-
{{ with .IssueCount.Open }}
-
<div class="flex gap-1 items-center text-sm">
-
{{ i "circle-dot" "w-3 h-3" }}
-
<span>{{ . }}</span>
-
</div>
-
{{ end }}
-
{{ with .PullCount.Open }}
-
<div class="flex gap-1 items-center text-sm">
-
{{ i "git-pull-request" "w-3 h-3" }}
-
<span>{{ . }}</span>
-
</div>
-
{{ end }}
-
</div>
{{ end }}
···
{{ with $repo }}
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
+
<div class="font-medium dark:text-white flex gap-2 items-center">
{{- if $fullName -}}
<a href="/{{ index $root.DidHandleMap .Did }}/{{ .Name }}">{{ index $root.DidHandleMap .Did }}/{{ .Name }}</a>
{{- else -}}
···
{{ end }}
{{ define "repoStats" }}
+
<div class="text-gray-400 pt-4 text-sm font-mono inline-flex gap-4 mt-auto">
+
{{ with .Language }}
+
<div class="flex gap-2 items-center text-sm">
+
<div class="size-2 rounded-full" style="background-color: {{ langColor . }};"></div>
+
<span>{{ . }}</span>
+
</div>
+
{{ end }}
+
{{ with .StarCount }}
+
<div class="flex gap-1 items-center text-sm">
+
{{ i "star" "w-3 h-3 fill-current" }}
+
<span>{{ . }}</span>
+
</div>
+
{{ end }}
+
{{ with .IssueCount.Open }}
+
<div class="flex gap-1 items-center text-sm">
+
{{ i "circle-dot" "w-3 h-3" }}
+
<span>{{ . }}</span>
+
</div>
+
{{ end }}
+
{{ with .PullCount.Open }}
+
<div class="flex gap-1 items-center text-sm">
+
{{ i "git-pull-request" "w-3 h-3" }}
+
<span>{{ . }}</span>
+
</div>
+
{{ end }}
+
</div>
{{ end }}
+5 -5
appview/pages/templates/user/profile.html
···
{{ end }}
{{ define "content" }}
-
<div class="grid grid-cols-1 md:grid-cols-8 gap-4">
-
<div class="md:col-span-2 order-1 md:order-1">
<div class="grid grid-cols-1 gap-4">
{{ template "user/fragments/profileCard" .Card }}
{{ block "punchcard" .Punchcard }} {{ end }}
</div>
</div>
-
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
<div class="grid grid-cols-1 gap-4">
{{ block "ownRepos" . }}{{ end }}
{{ block "collaboratingRepos" . }}{{ end }}
</div>
</div>
-
<div class="md:col-span-3 order-3 md:order-3">
{{ block "profileTimeline" . }}{{ end }}
</div>
</div>
···
</button>
{{ end }}
</div>
-
<div id="repos" class="grid grid-cols-1 gap-4">
{{ range .Repos }}
{{ template "user/fragments/repoCard" (list $ . false) }}
{{ else }}
···
{{ end }}
{{ define "content" }}
+
<div class="grid grid-cols-1 md:grid-cols-11 gap-4">
+
<div class="md:col-span-3 order-1 md:order-1">
<div class="grid grid-cols-1 gap-4">
{{ template "user/fragments/profileCard" .Card }}
{{ block "punchcard" .Punchcard }} {{ end }}
</div>
</div>
+
<div id="all-repos" class="md:col-span-4 order-2 md:order-2">
<div class="grid grid-cols-1 gap-4">
{{ block "ownRepos" . }}{{ end }}
{{ block "collaboratingRepos" . }}{{ end }}
</div>
</div>
+
<div class="md:col-span-4 order-3 md:order-3">
{{ block "profileTimeline" . }}{{ end }}
</div>
</div>
···
</button>
{{ end }}
</div>
+
<div id="repos" class="grid grid-cols-1 gap-4 items-stretch">
{{ range .Repos }}
{{ template "user/fragments/repoCard" (list $ . false) }}
{{ else }}