Monorepo for Tangled
at pr-page 768 lines 21 kB view raw
1package state 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "strings" 10 "time" 11 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 "github.com/go-chi/chi/v5" 17 "github.com/gorilla/feeds" 18 "tangled.org/core/api/tangled" 19 "tangled.org/core/appview/db" 20 "tangled.org/core/appview/models" 21 "tangled.org/core/appview/pages" 22 "tangled.org/core/orm" 23) 24 25func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 26 tabVal := r.URL.Query().Get("tab") 27 switch tabVal { 28 case "repos": 29 s.reposPage(w, r) 30 case "followers": 31 s.followersPage(w, r) 32 case "following": 33 s.followingPage(w, r) 34 case "starred": 35 s.starredPage(w, r) 36 case "strings": 37 s.stringsPage(w, r) 38 case "pulls": 39 s.pullsPage(w, r) 40 default: 41 s.profileOverview(w, r) 42 } 43} 44 45func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { 46 didOrHandle := chi.URLParam(r, "user") 47 if didOrHandle == "" { 48 return nil, fmt.Errorf("empty DID or handle") 49 } 50 51 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 52 if !ok { 53 return nil, fmt.Errorf("failed to resolve ID") 54 } 55 did := ident.DID.String() 56 57 profile, err := db.GetProfile(s.db, did) 58 if err != nil { 59 return nil, fmt.Errorf("failed to get profile: %w", err) 60 } 61 62 repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did)) 63 if err != nil { 64 return nil, fmt.Errorf("failed to get repo count: %w", err) 65 } 66 67 stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did)) 68 if err != nil { 69 return nil, fmt.Errorf("failed to get string count: %w", err) 70 } 71 72 starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did)) 73 if err != nil { 74 return nil, fmt.Errorf("failed to get starred repo count: %w", err) 75 } 76 77 followStats, err := db.GetFollowerFollowingCount(s.db, did) 78 if err != nil { 79 return nil, fmt.Errorf("failed to get follower stats: %w", err) 80 } 81 82 loggedInUser := s.oauth.GetUser(r) 83 followStatus := models.IsNotFollowing 84 if loggedInUser != nil { 85 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 86 } 87 88 now := time.Now() 89 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 90 punchcard, err := db.MakePunchcard( 91 s.db, 92 orm.FilterEq("did", did), 93 orm.FilterGte("date", startOfYear.Format(time.DateOnly)), 94 orm.FilterLte("date", now.Format(time.DateOnly)), 95 ) 96 if err != nil { 97 return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) 98 } 99 100 return &pages.ProfileCard{ 101 UserDid: did, 102 Profile: profile, 103 FollowStatus: followStatus, 104 Stats: pages.ProfileStats{ 105 RepoCount: repoCount, 106 StringCount: stringCount, 107 StarredCount: starredCount, 108 FollowersCount: followStats.Followers, 109 FollowingCount: followStats.Following, 110 }, 111 Punchcard: punchcard, 112 }, nil 113} 114 115func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { 116 l := s.logger.With("handler", "profileHomePage") 117 118 profile, err := s.profile(r) 119 if err != nil { 120 l.Error("failed to build profile card", "err", err) 121 s.pages.Error500(w) 122 return 123 } 124 l = l.With("profileDid", profile.UserDid) 125 126 repos, err := db.GetRepos( 127 s.db, 128 0, 129 orm.FilterEq("did", profile.UserDid), 130 ) 131 if err != nil { 132 l.Error("failed to fetch repos", "err", err) 133 } 134 135 // filter out ones that are pinned 136 pinnedRepos := []models.Repo{} 137 for i, r := range repos { 138 // if this is a pinned repo, add it 139 if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 140 pinnedRepos = append(pinnedRepos, r) 141 } 142 143 // if there are no saved pins, add the first 4 repos 144 if profile.Profile.IsPinnedReposEmpty() && i < 4 { 145 pinnedRepos = append(pinnedRepos, r) 146 } 147 } 148 149 collaboratingRepos, err := db.CollaboratingIn(s.db, profile.UserDid) 150 if err != nil { 151 l.Error("failed to fetch collaborating repos", "err", err) 152 } 153 154 pinnedCollaboratingRepos := []models.Repo{} 155 for _, r := range collaboratingRepos { 156 // if this is a pinned repo, add it 157 if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 158 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 159 } 160 } 161 162 timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 163 if err != nil { 164 l.Error("failed to create timeline", "err", err) 165 } 166 167 // populate commit counts in the timeline, using the punchcard 168 now := time.Now() 169 for _, p := range profile.Punchcard.Punches { 170 years := now.Year() - p.Date.Year() 171 months := int(now.Month() - p.Date.Month()) 172 monthsAgo := years*12 + months 173 if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) { 174 timeline.ByMonth[monthsAgo].Commits += p.Count 175 } 176 } 177 178 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 179 LoggedInUser: s.oauth.GetUser(r), 180 Card: profile, 181 Repos: pinnedRepos, 182 CollaboratingRepos: pinnedCollaboratingRepos, 183 ProfileTimeline: timeline, 184 }) 185} 186 187func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 188 l := s.logger.With("handler", "reposPage") 189 190 profile, err := s.profile(r) 191 if err != nil { 192 l.Error("failed to build profile card", "err", err) 193 s.pages.Error500(w) 194 return 195 } 196 l = l.With("profileDid", profile.UserDid) 197 198 repos, err := db.GetRepos( 199 s.db, 200 0, 201 orm.FilterEq("did", profile.UserDid), 202 ) 203 if err != nil { 204 l.Error("failed to get repos", "err", err) 205 s.pages.Error500(w) 206 return 207 } 208 209 err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 210 LoggedInUser: s.oauth.GetUser(r), 211 Repos: repos, 212 Card: profile, 213 }) 214} 215 216func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { 217 l := s.logger.With("handler", "starredPage") 218 219 profile, err := s.profile(r) 220 if err != nil { 221 l.Error("failed to build profile card", "err", err) 222 s.pages.Error500(w) 223 return 224 } 225 l = l.With("profileDid", profile.UserDid) 226 227 stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid)) 228 if err != nil { 229 l.Error("failed to get stars", "err", err) 230 s.pages.Error500(w) 231 return 232 } 233 var repos []models.Repo 234 for _, s := range stars { 235 repos = append(repos, *s.Repo) 236 } 237 238 err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 239 LoggedInUser: s.oauth.GetUser(r), 240 Repos: repos, 241 Card: profile, 242 }) 243} 244 245func (s *State) stringsPage(w http.ResponseWriter, r *http.Request) { 246 l := s.logger.With("handler", "stringsPage") 247 248 profile, err := s.profile(r) 249 if err != nil { 250 l.Error("failed to build profile card", "err", err) 251 s.pages.Error500(w) 252 return 253 } 254 l = l.With("profileDid", profile.UserDid) 255 256 strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid)) 257 if err != nil { 258 l.Error("failed to get strings", "err", err) 259 s.pages.Error500(w) 260 return 261 } 262 263 err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 264 LoggedInUser: s.oauth.GetUser(r), 265 Strings: strings, 266 Card: profile, 267 }) 268} 269 270func (s *State) pullsPage(w http.ResponseWriter, r *http.Request) { 271 l := s.logger.With("handler", "pullsPage") 272 273 profile, err := s.profile(r) 274 if err != nil { 275 l.Error("failed to build profile card", "err", err) 276 s.pages.Error500(w) 277 return 278 } 279 l = l.With("profileDid", profile.UserDid) 280 281 pulls, err := db.GetPullsByOwnerDid(s.db, profile.UserDid, "-6 months") 282 if err != nil { 283 l.Error("failed to get pulls", "err", err) 284 s.pages.Error500(w) 285 return 286 } 287 288 err = s.pages.ProfilePulls(w, pages.ProfilePullsParams{ 289 LoggedInUser: s.oauth.GetUser(r), 290 Pulls: pulls, 291 Card: profile, 292 }) 293} 294 295type FollowsPageParams struct { 296 Follows []pages.FollowCard 297 Card *pages.ProfileCard 298} 299 300func (s *State) followPage( 301 r *http.Request, 302 fetchFollows func(db.Execer, string) ([]models.Follow, error), 303 extractDid func(models.Follow) string, 304) (*FollowsPageParams, error) { 305 l := s.logger.With("handler", "reposPage") 306 307 profile, err := s.profile(r) 308 if err != nil { 309 return nil, err 310 } 311 l = l.With("profileDid", profile.UserDid) 312 313 loggedInUser := s.oauth.GetUser(r) 314 params := FollowsPageParams{ 315 Card: profile, 316 } 317 318 follows, err := fetchFollows(s.db, profile.UserDid) 319 if err != nil { 320 l.Error("failed to fetch follows", "err", err) 321 return &params, err 322 } 323 324 if len(follows) == 0 { 325 return &params, nil 326 } 327 328 followDids := make([]string, 0, len(follows)) 329 for _, follow := range follows { 330 followDids = append(followDids, extractDid(follow)) 331 } 332 333 profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids)) 334 if err != nil { 335 l.Error("failed to get profiles", "followDids", followDids, "err", err) 336 return &params, err 337 } 338 339 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) 340 if err != nil { 341 log.Printf("getting follow counts for %s: %s", followDids, err) 342 } 343 344 loggedInUserFollowing := make(map[string]struct{}) 345 if loggedInUser != nil { 346 following, err := db.GetFollowing(s.db, loggedInUser.Did) 347 if err != nil { 348 l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 349 return &params, err 350 } 351 loggedInUserFollowing = make(map[string]struct{}, len(following)) 352 for _, follow := range following { 353 loggedInUserFollowing[follow.SubjectDid] = struct{}{} 354 } 355 } 356 357 followCards := make([]pages.FollowCard, len(follows)) 358 for i, did := range followDids { 359 followStats := followStatsMap[did] 360 followStatus := models.IsNotFollowing 361 if _, exists := loggedInUserFollowing[did]; exists { 362 followStatus = models.IsFollowing 363 } else if loggedInUser != nil && loggedInUser.Did == did { 364 followStatus = models.IsSelf 365 } 366 367 var profile *models.Profile 368 if p, exists := profiles[did]; exists { 369 profile = p 370 } else { 371 profile = &models.Profile{} 372 profile.Did = did 373 } 374 followCards[i] = pages.FollowCard{ 375 LoggedInUser: loggedInUser, 376 UserDid: did, 377 FollowStatus: followStatus, 378 FollowersCount: followStats.Followers, 379 FollowingCount: followStats.Following, 380 Profile: profile, 381 } 382 } 383 384 params.Follows = followCards 385 386 return &params, nil 387} 388 389func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 390 followPage, err := s.followPage(r, db.GetFollowers, func(f models.Follow) string { return f.UserDid }) 391 if err != nil { 392 s.pages.Notice(w, "all-followers", "Failed to load followers") 393 return 394 } 395 396 s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 397 LoggedInUser: s.oauth.GetUser(r), 398 Followers: followPage.Follows, 399 Card: followPage.Card, 400 }) 401} 402 403func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 404 followPage, err := s.followPage(r, db.GetFollowing, func(f models.Follow) string { return f.SubjectDid }) 405 if err != nil { 406 s.pages.Notice(w, "all-following", "Failed to load following") 407 return 408 } 409 410 s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 411 LoggedInUser: s.oauth.GetUser(r), 412 Following: followPage.Follows, 413 Card: followPage.Card, 414 }) 415} 416 417func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 418 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 419 if !ok { 420 s.pages.Error404(w) 421 return 422 } 423 424 feed, err := s.getProfileFeed(r.Context(), &ident) 425 if err != nil { 426 s.pages.Error500(w) 427 return 428 } 429 430 if feed == nil { 431 return 432 } 433 434 atom, err := feed.ToAtom() 435 if err != nil { 436 s.pages.Error500(w) 437 return 438 } 439 440 w.Header().Set("content-type", "application/atom+xml") 441 w.Write([]byte(atom)) 442} 443 444func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 445 timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 446 if err != nil { 447 return nil, err 448 } 449 450 author := &feeds.Author{ 451 Name: fmt.Sprintf("@%s", id.Handle), 452 } 453 454 feed := feeds.Feed{ 455 Title: fmt.Sprintf("%s's timeline", author.Name), 456 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 457 Items: make([]*feeds.Item, 0), 458 Updated: time.UnixMilli(0), 459 Author: author, 460 } 461 462 for _, byMonth := range timeline.ByMonth { 463 if err := s.addPullRequestItems(ctx, &feed, byMonth.PullEvents.Items, author); err != nil { 464 return nil, err 465 } 466 if err := s.addIssueItems(ctx, &feed, byMonth.IssueEvents.Items, author); err != nil { 467 return nil, err 468 } 469 if err := s.addRepoItems(ctx, &feed, byMonth.RepoEvents, author); err != nil { 470 return nil, err 471 } 472 } 473 474 slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 475 return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 476 }) 477 478 if len(feed.Items) > 0 { 479 feed.Updated = feed.Items[0].Created 480 } 481 482 return &feed, nil 483} 484 485func (s *State) addPullRequestItems(ctx context.Context, feed *feeds.Feed, pulls []*models.Pull, author *feeds.Author) error { 486 for _, pull := range pulls { 487 owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 488 if err != nil { 489 return err 490 } 491 492 // Add pull request creation item 493 feed.Items = append(feed.Items, s.createPullRequestItem(pull, owner, author)) 494 } 495 return nil 496} 497 498func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*models.Issue, author *feeds.Author) error { 499 for _, issue := range issues { 500 owner, err := s.idResolver.ResolveIdent(ctx, issue.Repo.Did) 501 if err != nil { 502 return err 503 } 504 505 feed.Items = append(feed.Items, s.createIssueItem(issue, owner, author)) 506 } 507 return nil 508} 509 510func (s *State) addRepoItems(ctx context.Context, feed *feeds.Feed, repos []models.RepoEvent, author *feeds.Author) error { 511 for _, repo := range repos { 512 item, err := s.createRepoItem(ctx, repo, author) 513 if err != nil { 514 return err 515 } 516 feed.Items = append(feed.Items, item) 517 } 518 return nil 519} 520 521func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 522 return &feeds.Item{ 523 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 524 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 525 Created: pull.Created, 526 Author: author, 527 } 528} 529 530func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 531 return &feeds.Item{ 532 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 533 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 534 Created: issue.Created, 535 Author: author, 536 } 537} 538 539func (s *State) createRepoItem(ctx context.Context, repo models.RepoEvent, author *feeds.Author) (*feeds.Item, error) { 540 var title string 541 if repo.Source != nil { 542 sourceOwner, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 543 if err != nil { 544 return nil, err 545 } 546 title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, sourceOwner.Handle, repo.Source.Name, repo.Repo.Name) 547 } else { 548 title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 549 } 550 551 return &feeds.Item{ 552 Title: title, 553 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 554 Created: repo.Repo.Created, 555 Author: author, 556 }, nil 557} 558 559func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 560 user := s.oauth.GetUser(r) 561 562 err := r.ParseForm() 563 if err != nil { 564 log.Println("invalid profile update form", err) 565 s.pages.Notice(w, "update-profile", "Invalid form.") 566 return 567 } 568 569 profile, err := db.GetProfile(s.db, user.Did) 570 if err != nil { 571 log.Printf("getting profile data for %s: %s", user.Did, err) 572 } 573 574 profile.Description = r.FormValue("description") 575 profile.IncludeBluesky = r.FormValue("includeBluesky") == "on" 576 profile.Location = r.FormValue("location") 577 profile.Pronouns = r.FormValue("pronouns") 578 579 var links [5]string 580 for i := range 5 { 581 iLink := r.FormValue(fmt.Sprintf("link%d", i)) 582 links[i] = iLink 583 } 584 profile.Links = links 585 586 // Parse stats (exactly 2) 587 stat0 := r.FormValue("stat0") 588 stat1 := r.FormValue("stat1") 589 590 if stat0 != "" { 591 profile.Stats[0].Kind = models.VanityStatKind(stat0) 592 } 593 594 if stat1 != "" { 595 profile.Stats[1].Kind = models.VanityStatKind(stat1) 596 } 597 598 if err := db.ValidateProfile(s.db, profile); err != nil { 599 log.Println("invalid profile", err) 600 s.pages.Notice(w, "update-profile", err.Error()) 601 return 602 } 603 604 s.updateProfile(profile, w, r) 605} 606 607func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 608 user := s.oauth.GetUser(r) 609 610 err := r.ParseForm() 611 if err != nil { 612 log.Println("invalid profile update form", err) 613 s.pages.Notice(w, "update-profile", "Invalid form.") 614 return 615 } 616 617 profile, err := db.GetProfile(s.db, user.Did) 618 if err != nil { 619 log.Printf("getting profile data for %s: %s", user.Did, err) 620 } 621 622 i := 0 623 var pinnedRepos [6]syntax.ATURI 624 for key, values := range r.Form { 625 if i >= 6 { 626 log.Println("invalid pin update form", err) 627 s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.") 628 return 629 } 630 if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 { 631 aturi, err := syntax.ParseATURI(values[0]) 632 if err != nil { 633 log.Println("invalid profile update form", err) 634 s.pages.Notice(w, "update-profile", "Invalid form.") 635 return 636 } 637 pinnedRepos[i] = aturi 638 i++ 639 } 640 } 641 profile.PinnedRepos = pinnedRepos 642 643 s.updateProfile(profile, w, r) 644} 645 646func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 647 user := s.oauth.GetUser(r) 648 tx, err := s.db.BeginTx(r.Context(), nil) 649 if err != nil { 650 log.Println("failed to start transaction", err) 651 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 652 return 653 } 654 655 client, err := s.oauth.AuthorizedClient(r) 656 if err != nil { 657 log.Println("failed to get authorized client", err) 658 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 659 return 660 } 661 662 // yeah... lexgen dose not support syntax.ATURI in the record for some reason, 663 // nor does it support exact size arrays 664 var pinnedRepoStrings []string 665 for _, r := range profile.PinnedRepos { 666 pinnedRepoStrings = append(pinnedRepoStrings, r.String()) 667 } 668 669 var vanityStats []string 670 for _, v := range profile.Stats { 671 vanityStats = append(vanityStats, string(v.Kind)) 672 } 673 674 ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 675 var cid *string 676 if ex != nil { 677 cid = ex.Cid 678 } 679 680 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 681 Collection: tangled.ActorProfileNSID, 682 Repo: user.Did, 683 Rkey: "self", 684 Record: &lexutil.LexiconTypeDecoder{ 685 Val: &tangled.ActorProfile{ 686 Bluesky: profile.IncludeBluesky, 687 Description: &profile.Description, 688 Links: profile.Links[:], 689 Location: &profile.Location, 690 PinnedRepositories: pinnedRepoStrings, 691 Stats: vanityStats[:], 692 Pronouns: &profile.Pronouns, 693 }}, 694 SwapRecord: cid, 695 }) 696 if err != nil { 697 log.Println("failed to update profile", err) 698 s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.") 699 return 700 } 701 702 err = db.UpsertProfile(tx, profile) 703 if err != nil { 704 log.Println("failed to update profile", err) 705 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 706 return 707 } 708 709 s.notifier.UpdateProfile(r.Context(), profile) 710 711 s.pages.HxRedirect(w, "/"+user.Did) 712} 713 714func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 715 user := s.oauth.GetUser(r) 716 717 profile, err := db.GetProfile(s.db, user.Did) 718 if err != nil { 719 log.Printf("getting profile data for %s: %s", user.Did, err) 720 } 721 722 s.pages.EditBioFragment(w, pages.EditBioParams{ 723 LoggedInUser: user, 724 Profile: profile, 725 }) 726} 727 728func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 729 user := s.oauth.GetUser(r) 730 731 profile, err := db.GetProfile(s.db, user.Did) 732 if err != nil { 733 log.Printf("getting profile data for %s: %s", user.Did, err) 734 } 735 736 repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 737 if err != nil { 738 log.Printf("getting repos for %s: %s", user.Did, err) 739 } 740 741 collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 742 if err != nil { 743 log.Printf("getting collaborating repos for %s: %s", user.Did, err) 744 } 745 746 allRepos := []pages.PinnedRepo{} 747 748 for _, r := range repos { 749 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 750 allRepos = append(allRepos, pages.PinnedRepo{ 751 IsPinned: isPinned, 752 Repo: r, 753 }) 754 } 755 for _, r := range collaboratingRepos { 756 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 757 allRepos = append(allRepos, pages.PinnedRepo{ 758 IsPinned: isPinned, 759 Repo: r, 760 }) 761 } 762 763 s.pages.EditPinsFragment(w, pages.EditPinsParams{ 764 LoggedInUser: user, 765 Profile: profile, 766 AllRepos: allRepos, 767 }) 768}