forked from tangled.org/core
Monorepo for Tangled

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
··· 423 423 } 424 424 425 425 idx := idxs[did] 426 - log.Println("idx", "idx", idx, "link", link) 427 426 profileMap[did].Links[idx] = link 428 427 idxs[did] = idx + 1 429 428 }
+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
··· 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
··· 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
··· 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
··· 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 }}