appview/pages: rework followers/following/repos pages to use profile layout #541

merged
opened by oppi.li targeting master from push-mvmrzuxwmzvs
Changed files
+208 -201
appview
+2 -2
appview/db/repos.go
··· 310 311 slices.SortFunc(repos, func(a, b Repo) int { 312 if a.Created.After(b.Created) { 313 - return 1 314 } 315 - return -1 316 }) 317 318 return repos, nil
··· 310 311 slices.SortFunc(repos, func(a, b Repo) int { 312 if a.Created.After(b.Created) { 313 + return -1 314 } 315 + return 1 316 }) 317 318 return repos, nil
+26 -15
appview/pages/pages.go
··· 444 return p.executeProfile("user/overview", w, params) 445 } 446 447 - Profile *db.Profile 448 } 449 450 - func (p *Pages) ProfileHomePage(w io.Writer, params ProfileHomePageParams) error { 451 - return p.execute("user/profile", w, params) 452 } 453 454 - type ReposPageParams struct { 455 LoggedInUser *oauth.User 456 Repos []db.Repo 457 - Card ProfileCard 458 } 459 460 - func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 461 - return p.execute("user/repos", w, params) 462 } 463 464 type FollowCard struct { ··· 469 Profile *db.Profile 470 } 471 472 - type FollowersPageParams struct { 473 LoggedInUser *oauth.User 474 Followers []FollowCard 475 - Card ProfileCard 476 } 477 478 - func (p *Pages) FollowersPage(w io.Writer, params FollowersPageParams) error { 479 - return p.execute("user/followers", w, params) 480 } 481 482 - type FollowingPageParams struct { 483 LoggedInUser *oauth.User 484 Following []FollowCard 485 - Card ProfileCard 486 } 487 488 - func (p *Pages) FollowingPage(w io.Writer, params FollowingPageParams) error { 489 - return p.execute("user/following", w, params) 490 } 491 492 type FollowFragmentParams struct {
··· 444 return p.executeProfile("user/overview", w, params) 445 } 446 447 + type ProfileReposParams struct { 448 + LoggedInUser *oauth.User 449 + Repos []db.Repo 450 + Card *ProfileCard 451 + Active string 452 } 453 454 + func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 455 + params.Active = "repos" 456 + return p.executeProfile("user/repos", w, params) 457 } 458 459 + type ProfileStarredParams struct { 460 LoggedInUser *oauth.User 461 Repos []db.Repo 462 + Card *ProfileCard 463 + Active string 464 } 465 466 + func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 467 + params.Active = "starred" 468 + return p.executeProfile("user/starred", w, params) 469 } 470 471 type FollowCard struct { ··· 476 Profile *db.Profile 477 } 478 479 + type ProfileFollowersParams struct { 480 LoggedInUser *oauth.User 481 Followers []FollowCard 482 + Card *ProfileCard 483 + Active string 484 } 485 486 + func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 487 + params.Active = "overview" 488 + return p.executeProfile("user/followers", w, params) 489 } 490 491 + type ProfileFollowingParams struct { 492 LoggedInUser *oauth.User 493 Following []FollowCard 494 + Card *ProfileCard 495 + Active string 496 } 497 498 + func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 499 + params.Active = "overview" 500 + return p.executeProfile("user/following", w, params) 501 } 502 503 type FollowFragmentParams struct {
+4 -16
appview/pages/templates/user/followers.html
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · followers {{ end }} 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s followers" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=followers" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 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 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "followers" . }}{{ end }} 17 - </div> 18 - </div> 19 {{ end }} 20 21 {{ define "followers" }}
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · followers {{ end }} 2 3 + {{ define "profileContent" }} 4 + <div id="all-followers" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "followers" . }}{{ end }} 6 + </div> 7 {{ end }} 8 9 {{ define "followers" }}
+4 -16
appview/pages/templates/user/following.html
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · following {{ end }} 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s following" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=following" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 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 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "following" . }}{{ end }} 17 - </div> 18 - </div> 19 {{ end }} 20 21 {{ define "following" }}
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · following {{ end }} 2 3 + {{ define "profileContent" }} 4 + <div id="all-following" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "following" . }}{{ end }} 6 + </div> 7 {{ end }} 8 9 {{ define "following" }}
+7 -18
appview/pages/templates/user/repos.html
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · repos {{ end }} 2 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}'s repos" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh/{{ or .Card.UserHandle .Card.UserDid }}?tab=repos" /> 7 - <meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" /> 8 - {{ end }} 9 - 10 - {{ define "content" }} 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 - {{ template "user/fragments/profileCard" .Card }} 14 - </div> 15 - <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 16 - {{ block "ownRepos" . }}{{ end }} 17 - </div> 18 - </div> 19 {{ end }} 20 21 {{ define "ownRepos" }} 22 - <p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p> 23 <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 24 {{ range .Repos }} 25 - {{ template "user/fragments/repoCard" (list $ . false) }} 26 {{ else }} 27 <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 28 {{ end }}
··· 1 {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · repos {{ end }} 2 3 + {{ define "profileContent" }} 4 + <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "ownRepos" . }}{{ end }} 6 + </div> 7 {{ end }} 8 9 {{ define "ownRepos" }} 10 <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 11 {{ range .Repos }} 12 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "user/fragments/repoCard" (list $ . false) }} 14 + </div> 15 {{ else }} 16 <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 17 {{ end }}
+19
appview/pages/templates/user/starred.html
···
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} · repos {{ end }} 2 + 3 + {{ define "profileContent" }} 4 + <div id="all-repos" class="md:col-span-8 order-2 md:order-2"> 5 + {{ block "starredRepos" . }}{{ end }} 6 + </div> 7 + {{ end }} 8 + 9 + {{ define "starredRepos" }} 10 + <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 11 + {{ range .Repos }} 12 + <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 13 + {{ template "user/fragments/repoCard" (list $ . true) }} 14 + </div> 15 + {{ else }} 16 + <p class="px-6 dark:text-white">This user does not have any starred repos yet.</p> 17 + {{ end }} 18 + </div> 19 + {{ end }}
+146 -134
appview/state/profile.go
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 - "tangled.sh/tangled.sh/core/appview/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 ) 23 24 func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 25 tabVal := r.URL.Query().Get("tab") 26 switch tabVal { 27 - case "": 28 - s.profileHomePage(w, r) 29 case "repos": 30 s.reposPage(w, r) 31 case "followers": 32 s.followersPage(w, r) 33 case "following": 34 s.followingPage(w, r) 35 } 36 } 37 38 - type ProfilePageParams struct { 39 - Id identity.Identity 40 - LoggedInUser *oauth.User 41 - Card pages.ProfileCard 42 - } 43 - 44 - func (s *State) profilePage(w http.ResponseWriter, r *http.Request) *ProfilePageParams { 45 didOrHandle := chi.URLParam(r, "user") 46 if didOrHandle == "" { 47 - http.Error(w, "bad request", http.StatusBadRequest) 48 - return nil 49 } 50 51 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 52 if !ok { 53 - log.Printf("malformed middleware") 54 - w.WriteHeader(http.StatusInternalServerError) 55 - return nil 56 } 57 did := ident.DID.String() 58 59 profile, err := db.GetProfile(s.db, did) 60 if err != nil { 61 - log.Printf("getting profile data for %s: %s", did, err) 62 - s.pages.Error500(w) 63 - return nil 64 } 65 66 followStats, err := db.GetFollowerFollowingCount(s.db, did) 67 if err != nil { 68 - log.Printf("getting follow stats for %s: %s", did, err) 69 } 70 71 loggedInUser := s.oauth.GetUser(r) ··· 74 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 75 } 76 77 - return &ProfilePageParams{ 78 - Id: ident, 79 - LoggedInUser: loggedInUser, 80 - Card: pages.ProfileCard{ 81 - UserDid: did, 82 - UserHandle: ident.Handle.String(), 83 - Profile: profile, 84 - FollowStatus: followStatus, 85 - FollowersCount: followStats.Followers, 86 - FollowingCount: followStats.Following, 87 - }, 88 } 89 } 90 91 - func (s *State) profileHomePage(w http.ResponseWriter, r *http.Request) { 92 - pageWithProfile := s.profilePage(w, r) 93 - if pageWithProfile == nil { 94 return 95 } 96 97 - id := pageWithProfile.Id 98 repos, err := db.GetRepos( 99 s.db, 100 0, 101 - db.FilterEq("did", id.DID), 102 ) 103 if err != nil { 104 - log.Printf("getting repos for %s: %s", id.DID, err) 105 } 106 107 - profile := pageWithProfile.Card.Profile 108 // filter out ones that are pinned 109 pinnedRepos := []db.Repo{} 110 for i, r := range repos { 111 // if this is a pinned repo, add it 112 - if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 113 pinnedRepos = append(pinnedRepos, r) 114 } 115 116 // if there are no saved pins, add the first 4 repos 117 - if profile.IsPinnedReposEmpty() && i < 4 { 118 pinnedRepos = append(pinnedRepos, r) 119 } 120 } 121 122 - collaboratingRepos, err := db.CollaboratingIn(s.db, id.DID.String()) 123 if err != nil { 124 - log.Printf("getting collaborating repos for %s: %s", id.DID, err) 125 } 126 127 pinnedCollaboratingRepos := []db.Repo{} 128 for _, r := range collaboratingRepos { 129 // if this is a pinned repo, add it 130 - if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 131 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 132 } 133 } 134 135 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 136 if err != nil { 137 - log.Printf("failed to create profile timeline for %s: %s", id.DID, err) 138 } 139 140 - var didsToResolve []string 141 - for _, r := range collaboratingRepos { 142 - didsToResolve = append(didsToResolve, r.Did) 143 - } 144 - for _, byMonth := range timeline.ByMonth { 145 - for _, pe := range byMonth.PullEvents.Items { 146 - didsToResolve = append(didsToResolve, pe.Repo.Did) 147 - } 148 - for _, ie := range byMonth.IssueEvents.Items { 149 - didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 150 - } 151 - for _, re := range byMonth.RepoEvents { 152 - didsToResolve = append(didsToResolve, re.Repo.Did) 153 - if re.Source != nil { 154 - didsToResolve = append(didsToResolve, re.Source.Did) 155 - } 156 - } 157 } 158 159 - now := time.Now() 160 - startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 161 - punchcard, err := db.MakePunchcard( 162 s.db, 163 - db.FilterEq("did", id.DID), 164 - db.FilterGte("date", startOfYear.Format(time.DateOnly)), 165 - db.FilterLte("date", now.Format(time.DateOnly)), 166 ) 167 if err != nil { 168 - log.Println("failed to get punchcard for did", "did", id.DID, "err", err) 169 } 170 171 - s.pages.ProfileHomePage(w, pages.ProfileHomePageParams{ 172 - LoggedInUser: pageWithProfile.LoggedInUser, 173 - Repos: pinnedRepos, 174 - CollaboratingRepos: pinnedCollaboratingRepos, 175 - Card: pageWithProfile.Card, 176 - Punchcard: punchcard, 177 - ProfileTimeline: timeline, 178 }) 179 } 180 181 - func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 182 - pageWithProfile := s.profilePage(w, r) 183 - if pageWithProfile == nil { 184 return 185 } 186 187 - id := pageWithProfile.Id 188 repos, err := db.GetRepos( 189 s.db, 190 0, 191 - db.FilterEq("did", id.DID), 192 ) 193 if err != nil { 194 - log.Printf("getting repos for %s: %s", id.DID, err) 195 } 196 197 - s.pages.ReposPage(w, pages.ReposPageParams{ 198 - LoggedInUser: pageWithProfile.LoggedInUser, 199 Repos: repos, 200 - Card: pageWithProfile.Card, 201 }) 202 } 203 204 type FollowsPageParams struct { 205 - LoggedInUser *oauth.User 206 - Follows []pages.FollowCard 207 - Card pages.ProfileCard 208 } 209 210 - func (s *State) followPage(w http.ResponseWriter, r *http.Request, fetchFollows func(db.Execer, string) ([]db.Follow, error), extractDid func(db.Follow) string) (FollowsPageParams, error) { 211 - pageWithProfile := s.profilePage(w, r) 212 - if pageWithProfile == nil { 213 - return FollowsPageParams{}, nil 214 } 215 216 - id := pageWithProfile.Id 217 - loggedInUser := pageWithProfile.LoggedInUser 218 219 - follows, err := fetchFollows(s.db, id.DID.String()) 220 if err != nil { 221 - log.Printf("getting followers for %s: %s", id.DID, err) 222 - return FollowsPageParams{}, err 223 } 224 225 if len(follows) == 0 { 226 - return FollowsPageParams{ 227 - LoggedInUser: loggedInUser, 228 - Follows: []pages.FollowCard{}, 229 - Card: pageWithProfile.Card, 230 - }, nil 231 } 232 233 followDids := make([]string, 0, len(follows)) ··· 237 238 profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 239 if err != nil { 240 - log.Printf("getting profile for %s: %s", followDids, err) 241 - return FollowsPageParams{}, err 242 } 243 244 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) ··· 246 log.Printf("getting follow counts for %s: %s", followDids, err) 247 } 248 249 - var loggedInUserFollowing map[string]struct{} 250 if loggedInUser != nil { 251 following, err := db.GetFollowing(s.db, loggedInUser.Did) 252 if err != nil { 253 - return FollowsPageParams{}, err 254 } 255 - if len(following) > 0 { 256 - loggedInUserFollowing = make(map[string]struct{}, len(following)) 257 - for _, follow := range following { 258 - loggedInUserFollowing[follow.SubjectDid] = struct{}{} 259 - } 260 } 261 } 262 263 - followCards := make([]pages.FollowCard, 0, len(follows)) 264 - for _, did := range followDids { 265 - followStats, exists := followStatsMap[did] 266 - if !exists { 267 - followStats = db.FollowStats{} 268 - } 269 followStatus := db.IsNotFollowing 270 - if loggedInUserFollowing != nil { 271 - if _, exists := loggedInUserFollowing[did]; exists { 272 - followStatus = db.IsFollowing 273 - } else if loggedInUser.Did == did { 274 - followStatus = db.IsSelf 275 - } 276 } 277 var profile *db.Profile 278 if p, exists := profiles[did]; exists { 279 profile = p ··· 281 profile = &db.Profile{} 282 profile.Did = did 283 } 284 - followCards = append(followCards, pages.FollowCard{ 285 UserDid: did, 286 FollowStatus: followStatus, 287 FollowersCount: followStats.Followers, 288 FollowingCount: followStats.Following, 289 Profile: profile, 290 - }) 291 } 292 293 - return FollowsPageParams{ 294 - LoggedInUser: loggedInUser, 295 - Follows: followCards, 296 - Card: pageWithProfile.Card, 297 }, nil 298 } 299 300 func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 301 - followPage, err := s.followPage(w, r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 302 if err != nil { 303 s.pages.Notice(w, "all-followers", "Failed to load followers") 304 return 305 } 306 307 - s.pages.FollowersPage(w, pages.FollowersPageParams{ 308 - LoggedInUser: followPage.LoggedInUser, 309 Followers: followPage.Follows, 310 Card: followPage.Card, 311 }) 312 } 313 314 func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 315 - followPage, err := s.followPage(w, r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 316 if err != nil { 317 s.pages.Notice(w, "all-following", "Failed to load following") 318 return 319 } 320 321 - s.pages.FollowingPage(w, pages.FollowingPageParams{ 322 - LoggedInUser: followPage.LoggedInUser, 323 Following: followPage.Follows, 324 Card: followPage.Card, 325 })
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 + // "tangled.sh/tangled.sh/core/appview/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 ) 23 24 func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 25 tabVal := r.URL.Query().Get("tab") 26 switch tabVal { 27 + case "", "overview": 28 + s.profileOverview(w, r) 29 case "repos": 30 s.reposPage(w, r) 31 case "followers": 32 s.followersPage(w, r) 33 case "following": 34 s.followingPage(w, r) 35 + case "starred": 36 + s.starredPage(w, r) 37 } 38 } 39 40 + func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { 41 didOrHandle := chi.URLParam(r, "user") 42 if didOrHandle == "" { 43 + return nil, fmt.Errorf("empty DID or handle") 44 } 45 46 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 47 if !ok { 48 + return nil, fmt.Errorf("failed to resolve ID") 49 } 50 did := ident.DID.String() 51 52 profile, err := db.GetProfile(s.db, did) 53 if err != nil { 54 + return nil, fmt.Errorf("failed to get profile: %w", err) 55 } 56 57 followStats, err := db.GetFollowerFollowingCount(s.db, did) 58 if err != nil { 59 + return nil, fmt.Errorf("failed to get follower stats: %w", err) 60 } 61 62 loggedInUser := s.oauth.GetUser(r) ··· 65 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 66 } 67 68 + now := time.Now() 69 + startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 70 + punchcard, err := db.MakePunchcard( 71 + s.db, 72 + db.FilterEq("did", did), 73 + db.FilterGte("date", startOfYear.Format(time.DateOnly)), 74 + db.FilterLte("date", now.Format(time.DateOnly)), 75 + ) 76 + if err != nil { 77 + return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) 78 } 79 + 80 + return &pages.ProfileCard{ 81 + UserDid: did, 82 + UserHandle: ident.Handle.String(), 83 + Profile: profile, 84 + FollowStatus: followStatus, 85 + FollowersCount: followStats.Followers, 86 + FollowingCount: followStats.Following, 87 + Punchcard: punchcard, 88 + }, nil 89 } 90 91 + func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { 92 + l := s.logger.With("handler", "profileHomePage") 93 + 94 + profile, err := s.profile(r) 95 + if err != nil { 96 + l.Error("failed to build profile card", "err", err) 97 + s.pages.Error500(w) 98 return 99 } 100 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 101 102 repos, err := db.GetRepos( 103 s.db, 104 0, 105 + db.FilterEq("did", profile.UserDid), 106 ) 107 if err != nil { 108 + l.Error("failed to fetch repos", "err", err) 109 } 110 111 // filter out ones that are pinned 112 pinnedRepos := []db.Repo{} 113 for i, r := range repos { 114 // if this is a pinned repo, add it 115 + if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 116 pinnedRepos = append(pinnedRepos, r) 117 } 118 119 // if there are no saved pins, add the first 4 repos 120 + if profile.Profile.IsPinnedReposEmpty() && i < 4 { 121 pinnedRepos = append(pinnedRepos, r) 122 } 123 } 124 125 + collaboratingRepos, err := db.CollaboratingIn(s.db, profile.UserDid) 126 if err != nil { 127 + l.Error("failed to fetch collaborating repos", "err", err) 128 } 129 130 pinnedCollaboratingRepos := []db.Repo{} 131 for _, r := range collaboratingRepos { 132 // if this is a pinned repo, add it 133 + if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 134 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 135 } 136 } 137 138 + timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 139 if err != nil { 140 + l.Error("failed to create timeline", "err", err) 141 } 142 143 + s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 144 + LoggedInUser: s.oauth.GetUser(r), 145 + Card: profile, 146 + Repos: pinnedRepos, 147 + CollaboratingRepos: pinnedCollaboratingRepos, 148 + ProfileTimeline: timeline, 149 + }) 150 + } 151 + 152 + func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 153 + l := s.logger.With("handler", "reposPage") 154 + 155 + profile, err := s.profile(r) 156 + if err != nil { 157 + l.Error("failed to build profile card", "err", err) 158 + s.pages.Error500(w) 159 + return 160 } 161 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 162 163 + repos, err := db.GetRepos( 164 s.db, 165 + 0, 166 + db.FilterEq("did", profile.UserDid), 167 ) 168 if err != nil { 169 + l.Error("failed to get repos", "err", err) 170 + s.pages.Error500(w) 171 + return 172 } 173 174 + err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 175 + LoggedInUser: s.oauth.GetUser(r), 176 + Repos: repos, 177 + Card: profile, 178 }) 179 } 180 181 + func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { 182 + l := s.logger.With("handler", "starredPage") 183 + 184 + profile, err := s.profile(r) 185 + if err != nil { 186 + l.Error("failed to build profile card", "err", err) 187 + s.pages.Error500(w) 188 return 189 } 190 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 191 + 192 + stars, err := db.GetStars(s.db, 0, db.FilterEq("starred_by_did", profile.UserDid)) 193 + if err != nil { 194 + l.Error("failed to get stars", "err", err) 195 + s.pages.Error500(w) 196 + return 197 + } 198 + var repoAts []string 199 + for _, s := range stars { 200 + repoAts = append(repoAts, string(s.RepoAt)) 201 + } 202 203 repos, err := db.GetRepos( 204 s.db, 205 0, 206 + db.FilterIn("at_uri", repoAts), 207 ) 208 if err != nil { 209 + l.Error("failed to get repos", "err", err) 210 + s.pages.Error500(w) 211 + return 212 } 213 214 + err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 215 + LoggedInUser: s.oauth.GetUser(r), 216 Repos: repos, 217 + Card: profile, 218 }) 219 } 220 221 type FollowsPageParams struct { 222 + Follows []pages.FollowCard 223 + Card *pages.ProfileCard 224 } 225 226 + func (s *State) followPage( 227 + r *http.Request, 228 + fetchFollows func(db.Execer, string) ([]db.Follow, error), 229 + extractDid func(db.Follow) string, 230 + ) (*FollowsPageParams, error) { 231 + l := s.logger.With("handler", "reposPage") 232 + 233 + profile, err := s.profile(r) 234 + if err != nil { 235 + return nil, err 236 } 237 + l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 238 239 + loggedInUser := s.oauth.GetUser(r) 240 241 + follows, err := fetchFollows(s.db, profile.UserDid) 242 if err != nil { 243 + l.Error("failed to fetch follows", "err", err) 244 + return nil, err 245 } 246 247 if len(follows) == 0 { 248 + return nil, nil 249 } 250 251 followDids := make([]string, 0, len(follows)) ··· 255 256 profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 257 if err != nil { 258 + l.Error("failed to get profiles", "followDids", followDids, "err", err) 259 + return nil, err 260 } 261 262 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) ··· 264 log.Printf("getting follow counts for %s: %s", followDids, err) 265 } 266 267 + loggedInUserFollowing := make(map[string]struct{}) 268 if loggedInUser != nil { 269 following, err := db.GetFollowing(s.db, loggedInUser.Did) 270 if err != nil { 271 + l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 272 + return nil, err 273 } 274 + loggedInUserFollowing = make(map[string]struct{}, len(following)) 275 + for _, follow := range following { 276 + loggedInUserFollowing[follow.SubjectDid] = struct{}{} 277 } 278 } 279 280 + followCards := make([]pages.FollowCard, len(follows)) 281 + for i, did := range followDids { 282 + followStats := followStatsMap[did] 283 followStatus := db.IsNotFollowing 284 + if _, exists := loggedInUserFollowing[did]; exists { 285 + followStatus = db.IsFollowing 286 + } else if loggedInUser.Did == did { 287 + followStatus = db.IsSelf 288 } 289 + 290 var profile *db.Profile 291 if p, exists := profiles[did]; exists { 292 profile = p ··· 294 profile = &db.Profile{} 295 profile.Did = did 296 } 297 + followCards[i] = pages.FollowCard{ 298 UserDid: did, 299 FollowStatus: followStatus, 300 FollowersCount: followStats.Followers, 301 FollowingCount: followStats.Following, 302 Profile: profile, 303 + } 304 } 305 306 + return &FollowsPageParams{ 307 + Follows: followCards, 308 + Card: profile, 309 }, nil 310 } 311 312 func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 313 + followPage, err := s.followPage(r, db.GetFollowers, func(f db.Follow) string { return f.UserDid }) 314 if err != nil { 315 s.pages.Notice(w, "all-followers", "Failed to load followers") 316 return 317 } 318 319 + s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 320 + LoggedInUser: s.oauth.GetUser(r), 321 Followers: followPage.Follows, 322 Card: followPage.Card, 323 }) 324 } 325 326 func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 327 + followPage, err := s.followPage(r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid }) 328 if err != nil { 329 s.pages.Notice(w, "all-following", "Failed to load following") 330 return 331 } 332 333 + s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 334 + LoggedInUser: s.oauth.GetUser(r), 335 Following: followPage.Follows, 336 Card: followPage.Card, 337 })