forked from
tangled.org/core
Monorepo for Tangled
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 ¶ms, err
322 }
323
324 if len(follows) == 0 {
325 return ¶ms, 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 ¶ms, 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 ¶ms, 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 ¶ms, 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}