This repository has no description
1package pages
2
3import (
4 "context"
5 "crypto/sha256"
6 "embed"
7 "encoding/hex"
8 "fmt"
9 "html/template"
10 "io"
11 "io/fs"
12 "log/slog"
13 "net/http"
14 "os"
15 "path/filepath"
16 "strings"
17 "sync"
18 "time"
19
20 "tangled.org/core/appview/cache"
21 "tangled.org/core/appview/commitverify"
22 "tangled.org/core/appview/config"
23 "tangled.org/core/appview/db"
24 "tangled.org/core/appview/models"
25 "tangled.org/core/appview/oauth"
26 "tangled.org/core/appview/pages/markup"
27 "tangled.org/core/appview/pages/markup/sanitizer"
28 "tangled.org/core/appview/pages/repoinfo"
29 "tangled.org/core/appview/pagination"
30 "tangled.org/core/idresolver"
31 "tangled.org/core/types"
32
33 "github.com/bluesky-social/indigo/atproto/identity"
34 "github.com/bluesky-social/indigo/atproto/syntax"
35 "github.com/go-git/go-git/v5/plumbing"
36 "github.com/sourcegraph/zoekt"
37)
38
39//go:embed templates/* static legal
40var Files embed.FS
41
42type baseParamsCtxKey struct{}
43
44type BaseParams struct {
45 LoggedInUser *oauth.MultiAccountUser
46 FocusParams FocusParams
47 ThemePreference models.ThemePreference
48}
49
50type FocusParams struct {
51 Focusing bool
52 FocusLink string
53 FocusNotificationID int64
54 CurrentPath string // r.URL.Path, for off-focus detection in templates
55 FocusCount int // total unread focus-eligible items remaining
56}
57
58func (p *Pages) Resolver() *idresolver.Resolver {
59 return p.resolver
60}
61
62func BaseParamsIntoContext(ctx context.Context, bp BaseParams) context.Context {
63 return context.WithValue(ctx, baseParamsCtxKey{}, bp)
64}
65
66func BaseParamsFromContext(ctx context.Context) BaseParams {
67 bp, _ := ctx.Value(baseParamsCtxKey{}).(BaseParams)
68 return bp
69}
70
71type Pages struct {
72 mu sync.RWMutex
73 cache *TmplCache[string, *template.Template]
74
75 avatar config.AvatarConfig
76 pdsCfg config.PdsConfig
77 resolver *idresolver.Resolver
78 db *db.DB
79 rdb *cache.Cache
80 dev bool
81 embedFS fs.FS
82 templateDir string // Path to templates on disk for dev mode
83 rctx *markup.RenderContext
84 logger *slog.Logger
85}
86
87func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages {
88 // initialized with safe defaults, can be overridden per use
89 rctx := &markup.RenderContext{
90 IsDev: config.Core.Dev,
91 Hostname: config.Core.AppviewHost,
92 CamoUrl: config.Camo.Host,
93 CamoSecret: config.Camo.SharedSecret,
94 Directory: res.Directory(),
95 }
96
97 p := &Pages{
98 mu: sync.RWMutex{},
99 cache: NewTmplCache[string, *template.Template](),
100 dev: config.Core.Dev,
101 avatar: config.Avatar,
102 pdsCfg: config.Pds,
103 rctx: rctx,
104 resolver: res,
105 db: database,
106 rdb: rdb,
107 templateDir: "appview/pages",
108 logger: logger,
109 }
110
111 if p.dev {
112 p.embedFS = os.DirFS(p.templateDir)
113 } else {
114 p.embedFS = Files
115 }
116
117 return p
118}
119
120// reverse of pathToName
121func (p *Pages) nameToPath(s string) string {
122 return "templates/" + s + ".html"
123}
124
125// FuncMap returns the template function map for use by external template consumers.
126func (p *Pages) FuncMap() template.FuncMap {
127 return p.funcMap()
128}
129
130// FragmentPaths returns all fragment template paths from the embedded FS.
131func (p *Pages) FragmentPaths() ([]string, error) {
132 return p.fragmentPaths()
133}
134
135// EmbedFS returns the embedded filesystem containing templates and static assets.
136func (p *Pages) EmbedFS() fs.FS {
137 return p.embedFS
138}
139
140func (p *Pages) fragmentPaths() ([]string, error) {
141 var fragmentPaths []string
142 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
143 if err != nil {
144 return err
145 }
146 if d.IsDir() {
147 return nil
148 }
149 if !strings.HasSuffix(path, ".html") {
150 return nil
151 }
152 if !strings.Contains(path, "fragments/") {
153 return nil
154 }
155 fragmentPaths = append(fragmentPaths, path)
156 return nil
157 })
158 if err != nil {
159 return nil, err
160 }
161
162 return fragmentPaths, nil
163}
164
165// parse without memoization
166func (p *Pages) rawParse(stack ...string) (*template.Template, error) {
167 paths, err := p.fragmentPaths()
168 if err != nil {
169 return nil, err
170 }
171 for _, s := range stack {
172 paths = append(paths, p.nameToPath(s))
173 }
174
175 funcs := p.funcMap()
176 top := stack[len(stack)-1]
177 parsed, err := template.New(top).
178 Funcs(funcs).
179 ParseFS(p.embedFS, paths...)
180 if err != nil {
181 return nil, err
182 }
183
184 return parsed, nil
185}
186
187func (p *Pages) parse(stack ...string) (*template.Template, error) {
188 key := strings.Join(stack, "|")
189
190 // never cache in dev mode
191 if cached, exists := p.cache.Get(key); !p.dev && exists {
192 return cached, nil
193 }
194
195 result, err := p.rawParse(stack...)
196 if err != nil {
197 return nil, err
198 }
199
200 p.cache.Set(key, result)
201 return result, nil
202}
203
204func (p *Pages) parseBase(top string) (*template.Template, error) {
205 stack := []string{
206 "layouts/base",
207 top,
208 }
209 return p.parse(stack...)
210}
211
212func (p *Pages) parseRepoBase(top string) (*template.Template, error) {
213 stack := []string{
214 "layouts/base",
215 "layouts/repobase",
216 top,
217 }
218 return p.parse(stack...)
219}
220
221func (p *Pages) parseProfileBase(top string) (*template.Template, error) {
222 stack := []string{
223 "layouts/base",
224 "layouts/profilebase",
225 top,
226 }
227 return p.parse(stack...)
228}
229
230func (p *Pages) parseLoginBase(top string) (*template.Template, error) {
231 stack := []string{
232 "layouts/base",
233 "layouts/loginbase",
234 top,
235 }
236 return p.parse(stack...)
237}
238
239func (p *Pages) parseOnboardingBase(top string) (*template.Template, error) {
240 stack := []string{
241 "layouts/base",
242 "layouts/onboardingbase",
243 top,
244 }
245 return p.parse(stack...)
246}
247
248func (p *Pages) executePlain(name string, w io.Writer, params any) error {
249 tpl, err := p.parse(name)
250 if err != nil {
251 return err
252 }
253
254 err = tpl.Execute(w, params)
255 if err != nil {
256 p.logger.Error("failed to execute template", "template", name, "err", err)
257 }
258 return err
259}
260
261func (p *Pages) executeLogin(name string, w io.Writer, params any) error {
262 tpl, err := p.parseLoginBase(name)
263 if err != nil {
264 return err
265 }
266
267 err = tpl.ExecuteTemplate(w, "layouts/base", params)
268 if err != nil {
269 p.logger.Error("failed to execute login template", "template", name, "err", err)
270 }
271 return err
272}
273
274func (p *Pages) executeOnboarding(name string, w io.Writer, params any) error {
275 tpl, err := p.parseOnboardingBase(name)
276 if err != nil {
277 return err
278 }
279
280 return tpl.ExecuteTemplate(w, "layouts/base", params)
281}
282
283func (p *Pages) execute(name string, w io.Writer, params any) error {
284 tpl, err := p.parseBase(name)
285 if err != nil {
286 return err
287 }
288
289 err = tpl.ExecuteTemplate(w, "layouts/base", params)
290 if err != nil {
291 p.logger.Error("failed to execute template", "template", name, "err", err)
292 }
293 return err
294}
295
296func (p *Pages) executeRepo(name string, w io.Writer, params any) error {
297 tpl, err := p.parseRepoBase(name)
298 if err != nil {
299 return err
300 }
301
302 err = tpl.ExecuteTemplate(w, "layouts/base", params)
303 if err != nil {
304 p.logger.Error("failed to execute repo template", "template", name, "err", err)
305 }
306 return err
307}
308
309func (p *Pages) executeProfile(name string, w io.Writer, params any) error {
310 tpl, err := p.parseProfileBase(name)
311 if err != nil {
312 return err
313 }
314
315 err = tpl.ExecuteTemplate(w, "layouts/base", params)
316 if err != nil {
317 p.logger.Error("failed to execute profile template", "template", name, "err", err)
318 }
319 return err
320}
321
322type DollyParams struct {
323 Classes string
324 FillColor string
325 // Favicon embeds a prefers-color-scheme style block so the SVG
326 // adapts to dark mode when used as a standalone favicon document.
327 Favicon bool
328}
329
330func (p *Pages) Dolly(w io.Writer, params DollyParams) error {
331 return p.executePlain("fragments/dolly/logo", w, params)
332}
333
334func (p *Pages) Favicon(w io.Writer) error {
335 return p.Dolly(w, DollyParams{
336 Favicon: true,
337 })
338}
339
340type LoginParams struct {
341 BaseParams
342 ReturnUrl string
343 ErrorCode string
344 AddAccount bool
345 Handle string
346 Accounts []oauth.AccountInfo
347}
348
349func (p *Pages) Login(w io.Writer, params LoginParams) error {
350 return p.executeLogin("user/login", w, params)
351}
352
353type SignupParams struct {
354 BaseParams
355 CloudflareSiteKey string
356 EmailId string
357}
358
359func (p *Pages) Signup(w io.Writer, params SignupParams) error {
360 return p.executeLogin("user/signup", w, params)
361}
362
363func (p *Pages) CompleteSignup(w io.Writer) error {
364 return p.executeLogin("user/completeSignup", w, BaseParams{})
365}
366
367type SignupSuccessParams struct {
368 Handle string
369}
370
371func (p *Pages) SignupSuccess(w io.Writer, params SignupSuccessParams) error {
372 return p.executePlain("user/fragments/signupSuccess", w, params)
373}
374
375type TermsOfServiceParams struct {
376 BaseParams
377 Content template.HTML
378}
379
380func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error {
381 filename := "terms.md"
382 filePath := filepath.Join("legal", filename)
383
384 file, err := p.embedFS.Open(filePath)
385 if err != nil {
386 return fmt.Errorf("failed to read %s: %w", filename, err)
387 }
388 defer file.Close()
389
390 markdownBytes, err := io.ReadAll(file)
391 if err != nil {
392 return fmt.Errorf("failed to read %s: %w", filename, err)
393 }
394
395 rctx := p.rctx.Clone()
396 rctx.RendererType = markup.RendererTypeDefault
397 htmlString := rctx.RenderMarkdown(string(markdownBytes))
398 sanitized := sanitizer.SanitizeDefault(htmlString)
399 params.Content = template.HTML(sanitized)
400
401 return p.execute("legal/terms", w, params)
402}
403
404type PrivacyPolicyParams struct {
405 BaseParams
406 Content template.HTML
407}
408
409func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error {
410 filename := "privacy.md"
411 filePath := filepath.Join("legal", filename)
412
413 file, err := p.embedFS.Open(filePath)
414 if err != nil {
415 return fmt.Errorf("failed to read %s: %w", filename, err)
416 }
417 defer file.Close()
418
419 markdownBytes, err := io.ReadAll(file)
420 if err != nil {
421 return fmt.Errorf("failed to read %s: %w", filename, err)
422 }
423
424 rctx := p.rctx.Clone()
425 rctx.RendererType = markup.RendererTypeDefault
426 htmlString := rctx.RenderMarkdown(string(markdownBytes))
427 sanitized := sanitizer.SanitizeDefault(htmlString)
428 params.Content = template.HTML(sanitized)
429
430 return p.execute("legal/privacy", w, params)
431}
432
433type BrandParams struct {
434 BaseParams
435}
436
437func (p *Pages) Brand(w io.Writer, params BrandParams) error {
438 return p.execute("brand/brand", w, params)
439}
440
441type RecentItem struct {
442 Link *models.RecentLink
443 Repo *models.Repo
444 Issue *models.Issue
445 Pull *models.Pull
446}
447
448type BlogPost struct {
449 Slug string
450 Title string
451 Subtitle string
452 Date time.Time
453}
454
455type TimelineParams struct {
456 BaseParams
457 Onboarding models.OnboardingProgress
458 Timeline []models.TimelineGroup
459 Repos []models.Repo
460 GfiLabel *models.LabelDefinition
461 BlueskyPosts []models.BskyPost
462 VouchSuggestions []models.VouchSuggestion
463 Notifications []*models.NotificationWithEntity
464 Recents []RecentItem
465 FollowingOnly bool
466 RecentBlogPosts []BlogPost
467 // ShowNewsletter controls whether the newsletter widget/CTA is rendered.
468 // For logged-in users it reflects their newsletter_preferences row; for
469 // anonymous visitors it is always true (dismissal falls back to
470 // localStorage on the client).
471 ShowNewsletter bool
472 CanFocus bool
473}
474
475func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
476 return p.execute("timeline/timeline", w, params)
477}
478
479type GoodFirstIssuesParams struct {
480 BaseParams
481 Issues []models.Issue
482 RepoGroups []*models.RepoGroup
483 LabelDefs map[string]*models.LabelDefinition
484 GfiLabel *models.LabelDefinition
485 Page pagination.Page
486}
487
488func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error {
489 return p.execute("goodfirstissues/index", w, params)
490}
491
492type UserProfileSettingsParams struct {
493 BaseParams
494 Tab string
495 PunchcardPreference models.PunchcardPreference
496 IsTnglSh bool
497 IsDeactivated bool
498 HandleOpen bool
499}
500
501func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error {
502 params.Tab = "profile"
503 return p.execute("user/settings/profile", w, params)
504}
505
506type GroupedNotifications struct {
507 Today []*models.NotificationWithEntity
508 ThisWeek []*models.NotificationWithEntity
509 Older []*models.NotificationWithEntity
510}
511
512func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications {
513 now := time.Now()
514 todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
515 weekStart := todayStart.AddDate(0, 0, -6)
516
517 var g GroupedNotifications
518 for _, n := range notifs {
519 switch {
520 case !n.Created.Before(todayStart):
521 g.Today = append(g.Today, n)
522 case !n.Created.Before(weekStart):
523 g.ThisWeek = append(g.ThisWeek, n)
524 default:
525 g.Older = append(g.Older, n)
526 }
527 }
528 return g
529}
530
531type NotificationsParams struct {
532 BaseParams
533 WorkGroups GroupedNotifications
534 SocialGroups GroupedNotifications
535 MobileGroups GroupedNotifications
536 WorkUnreadCount int64
537 SocialUnreadCount int64
538 Page pagination.Page
539 Total int
540 ReadFilter string // "inbox" or "unread"
541 CategoryFilter string // "all", "work", "social"
542 CanFocus bool
543}
544
545func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error {
546 return p.execute("notifications/list", w, params)
547}
548
549func (p *Pages) NotificationItem(w io.Writer, notif *models.NotificationWithEntity) error {
550 return p.executePlain("notifications/fragments/item", w, notif)
551}
552
553type NotificationCountParams struct {
554 Count int64
555}
556
557func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error {
558 return p.executePlain("notifications/fragments/count", w, params)
559}
560
561type NotificationPreviewParams struct {
562 BaseParams
563 Notifications []*models.NotificationWithEntity
564 ReadFilter string
565 CategoryFilter string
566 CanFocus bool
567}
568
569func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error {
570 return p.executePlain("notifications/fragments/preview", w, params)
571}
572
573type UserKeysSettingsParams struct {
574 BaseParams
575 PubKeys []models.PublicKey
576 Tab string
577}
578
579func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error {
580 params.Tab = "keys"
581 return p.execute("user/settings/keys", w, params)
582}
583
584type UserEmailsSettingsParams struct {
585 BaseParams
586 Emails []models.Email
587 Tab string
588}
589
590func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error {
591 params.Tab = "emails"
592 return p.execute("user/settings/emails", w, params)
593}
594
595type UserNotificationSettingsParams struct {
596 BaseParams
597 Preferences *models.NotificationPreferences
598 HasVerifiedEmail bool
599 Tab string
600}
601
602func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error {
603 params.Tab = "notifications"
604 return p.execute("user/settings/notifications", w, params)
605}
606
607type UserSiteSettingsParams struct {
608 BaseParams
609 Claim *models.DomainClaim
610 SitesDomain string
611 IsTnglHandle bool
612 Tab string
613}
614
615func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error {
616 params.Tab = "sites"
617 return p.execute("user/settings/sites", w, params)
618}
619
620type UpgradeBannerParams struct {
621 Registrations []models.Registration
622 Spindles []models.Spindle
623}
624
625func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error {
626 return p.executePlain("banner", w, params)
627}
628
629type NewsletterResponseParams struct {
630 // Id identifies the calling form instance; the response span's id will
631 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target.
632 Id string
633 // Error, when non-empty, switches the template to the error variant.
634 Error string
635}
636
637func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error {
638 return p.executePlain("timeline/fragments/newsletterResponse", w, params)
639}
640
641type KnotsParams struct {
642 BaseParams
643 Knots []KnotListingParams
644 Tab string
645}
646
647func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
648 params.Tab = "knots"
649 return p.execute("knots/index", w, params)
650}
651
652type KnotParams struct {
653 BaseParams
654 Registration *models.Registration
655 Members []string
656 Repos map[string][]models.Repo
657 IsOwner bool
658 RepoCount int
659 Tab string
660}
661
662func (p *Pages) Knot(w io.Writer, params KnotParams) error {
663 return p.execute("knots/dashboard", w, params)
664}
665
666type KnotListingParams struct {
667 *models.Registration
668 RepoCount int
669}
670
671func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
672 return p.executePlain("knots/fragments/knotListing", w, params)
673}
674
675type SpindlesParams struct {
676 BaseParams
677 Tab string
678}
679
680func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
681 params.Tab = "spindles"
682 return p.execute("spindles/index", w, params)
683}
684
685type NewRepoParams struct {
686 BaseParams
687 Knots []string
688 Spindles []string
689}
690
691func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error {
692 return p.execute("repo/new", w, params)
693}
694
695type ForkRepoParams struct {
696 BaseParams
697 Knots []string
698 Spindles []string
699 RepoInfo repoinfo.RepoInfo
700}
701
702func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error {
703 return p.execute("repo/fork", w, params)
704}
705
706type ProfileCard struct {
707 UserDid string
708 HasProfile bool
709 IsTangledUser bool
710 FollowStatus models.FollowStatus
711 VouchRelationship *models.VouchRelationship
712 Punchcard *models.Punchcard
713 Profile *models.Profile
714 Stats ProfileStats
715 Active string
716 ProfileScript string
717}
718
719type ProfileStats struct {
720 RepoCount int64
721 StarredCount int64
722 StringCount int64
723 FollowersCount int64
724 FollowingCount int64
725}
726
727func (p *ProfileCard) GetTabs() [][]any {
728 tabs := [][]any{
729 {"overview", "overview", "square-chart-gantt", nil},
730 {"repos", "repos", "book-marked", p.Stats.RepoCount},
731 {"starred", "starred", "star", p.Stats.StarredCount},
732 {"strings", "strings", "line-squiggle", p.Stats.StringCount},
733 {"vouches", "vouches", "shield", nil},
734 }
735
736 return tabs
737}
738
739type ProfileOverviewParams struct {
740 BaseParams
741 Repos []models.Repo
742 CollaboratingRepos []models.Repo
743 ProfileTimeline *models.ProfileTimeline
744 Card *ProfileCard
745 Active string
746 ShowPunchcard bool
747}
748
749func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
750 params.Active = "overview"
751 return p.executeProfile("user/overview", w, params)
752}
753
754type ProfileReposParams struct {
755 BaseParams
756 Repos []models.Repo
757 StarStatuses map[string]bool
758 Card *ProfileCard
759 Active string
760 Page pagination.Page
761 RepoCount int
762 FilterQuery string
763}
764
765func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error {
766 params.Active = "repos"
767 return p.executeProfile("user/repos", w, params)
768}
769
770type ProfileStarredParams struct {
771 BaseParams
772 Repos []models.Repo
773 Card *ProfileCard
774 Page pagination.Page
775 Total int
776 Active string
777}
778
779func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error {
780 params.Active = "starred"
781 return p.executeProfile("user/starred", w, params)
782}
783
784type ProfileStringsParams struct {
785 BaseParams
786 Strings []models.String
787 Card *ProfileCard
788 Active string
789}
790
791func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error {
792 params.Active = "strings"
793 return p.executeProfile("user/strings", w, params)
794}
795
796type ProfileVouchesParams struct {
797 BaseParams
798 Vouches []models.Vouch
799 Suggestions []models.VouchSuggestion
800 Card *ProfileCard
801 Page pagination.Page
802 VouchCount int
803 Active string
804 EvidencePulls map[syntax.ATURI]*models.Pull
805 EvidenceIssues map[syntax.ATURI]*models.Issue
806}
807
808func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error {
809 params.Active = "vouches"
810 return p.executeProfile("user/vouches", w, params)
811}
812
813type FollowCard struct {
814 UserDid string
815 BaseParams
816 FollowStatus models.FollowStatus
817 FollowersCount int64
818 FollowingCount int64
819 Profile *models.Profile
820}
821
822type ProfileFollowersParams struct {
823 BaseParams
824 Followers []FollowCard
825 Card *ProfileCard
826 Active string
827}
828
829func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error {
830 params.Active = "overview"
831 return p.executeProfile("user/followers", w, params)
832}
833
834type ProfileFollowingParams struct {
835 BaseParams
836 Following []FollowCard
837 Card *ProfileCard
838 Active string
839}
840
841func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error {
842 params.Active = "overview"
843 return p.executeProfile("user/following", w, params)
844}
845
846type FollowFragmentParams struct {
847 UserDid string
848 FollowStatus models.FollowStatus
849 FollowersCount int64
850 HxSwapOob struct{} // empty struct so always truthy
851}
852
853func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
854 return p.executePlain("user/fragments/follow-oob", w, params)
855}
856
857type ProfilePopoverParams struct {
858 BaseParams
859 UserDid string
860 Profile *models.Profile
861 FollowStatus models.FollowStatus
862 VouchRelationship *models.VouchRelationship
863 Stats ProfilePopoverStats
864}
865
866type ProfilePopoverStats struct {
867 FollowersCount int64
868 FollowingCount int64
869}
870
871func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error {
872 return p.executePlain("user/fragments/profilePopover", w, params)
873}
874
875type EditBioParams struct {
876 BaseParams
877 Profile *models.Profile
878 AlsoKnownAs []string
879 // Action optionally overrides the form's hx-post target. Defaults to
880 // /profile/bio when empty. Used by the onboarding flow to save + advance.
881 Action string
882}
883
884func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error {
885 return p.executePlain("user/fragments/editBio", w, params)
886}
887
888type EditPinsParams struct {
889 BaseParams
890 Profile *models.Profile
891 AllRepos []PinnedRepo
892}
893
894type PinnedRepo struct {
895 IsPinned bool
896 models.Repo
897}
898
899func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error {
900 return p.executePlain("user/fragments/editPins", w, params)
901}
902
903type StarBtnFragmentParams struct {
904 IsStarred bool
905 SubjectAt syntax.ATURI
906 StarCount int
907 RepoName string
908 HxSwapOob bool
909}
910
911func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
912 params.HxSwapOob = true
913 return p.executePlain("fragments/starBtn", w, params)
914}
915
916type OnboardingParams struct {
917 BaseParams
918 Step int
919
920 EditBio EditBioParams
921
922 PubKeys []models.PublicKey
923
924 People []FollowCard
925 TrendingRepos []models.Repo
926 StarStatuses map[string]bool
927}
928
929// KeyFragment renders a single SSH key row (used to append a newly added key
930// to the list without a full reload).
931func (p *Pages) KeyFragment(w io.Writer, key models.PublicKey) error {
932 return p.executePlain("user/settings/fragments/keyListing", w, key)
933}
934
935func (p *Pages) Onboarding(w io.Writer, params OnboardingParams) error {
936 return p.executeOnboarding("onboarding/welcome", w, params)
937}
938
939type RepoIndexParams struct {
940 BaseParams
941 RepoInfo repoinfo.RepoInfo
942 Active string
943 TagMap map[string][]string
944 CommitsTrunc []types.Commit
945 TagsTrunc []*types.TagReference
946 BranchesTrunc []types.Branch
947 // ForkInfo *types.ForkInfo
948 HTMLReadme template.HTML
949 Raw bool
950 EmailToDid map[string]string
951 VerifiedCommits commitverify.VerifiedCommits
952 Languages []types.RepoLanguageDetails
953 NeedsKnotUpgrade bool
954 KnotUnreachable bool
955 types.RepoIndexResponse
956}
957
958func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error {
959 params.Active = "overview"
960 if params.IsEmpty {
961 return p.executeRepo("repo/empty", w, params)
962 }
963
964 if params.NeedsKnotUpgrade {
965 return p.executeRepo("repo/needsUpgrade", w, params)
966 }
967
968 if params.KnotUnreachable {
969 return p.executeRepo("repo/knotUnreachable", w, params)
970 }
971
972 rctx := p.rctx.Clone()
973 rctx.RepoInfo = params.RepoInfo
974 rctx.RepoInfo.Ref = params.Ref
975 rctx.RendererType = markup.RendererTypeRepoMarkdown
976
977 if params.ReadmeFileName != "" {
978 switch markup.GetFormat(params.ReadmeFileName) {
979 case markup.FormatMarkdown:
980 params.Raw = false
981 htmlString := rctx.RenderMarkdown(params.Readme)
982 sanitized := sanitizer.SanitizeDefault(htmlString)
983 params.HTMLReadme = template.HTML(sanitized)
984 default:
985 params.Raw = true
986 }
987 }
988
989 return p.executeRepo("repo/index", w, params)
990}
991
992type RepoSearchParams struct {
993 BaseParams
994 RepoInfo repoinfo.RepoInfo
995 Active string
996 FilterQuery string
997}
998
999func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error {
1000 params.Active = "overview"
1001 return p.executeRepo("repo/search", w, params)
1002}
1003
1004type RepoSearchResultsFragmentParams struct {
1005 Query string
1006 Results []SearchResult
1007 ErrorMsg string
1008}
1009
1010func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error {
1011 return p.executePlain("repo/fragments/searchResults", w, params)
1012}
1013
1014type RepoLogParams struct {
1015 BaseParams
1016 RepoInfo repoinfo.RepoInfo
1017 TagMap map[string][]string
1018 Active string
1019 EmailToDid map[string]string
1020 VerifiedCommits commitverify.VerifiedCommits
1021
1022 types.RepoLogResponse
1023}
1024
1025func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error {
1026 params.Active = "overview"
1027 return p.executeRepo("repo/log", w, params)
1028}
1029
1030type RepoCommitParams struct {
1031 BaseParams
1032 RepoInfo repoinfo.RepoInfo
1033 Active string
1034 EmailToDid map[string]string
1035 Pipeline *types.Pipeline
1036 DiffOpts types.DiffOpts
1037
1038 // singular because it's always going to be just one
1039 VerifiedCommit commitverify.VerifiedCommits
1040
1041 types.RepoCommitResponse
1042}
1043
1044func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error {
1045 params.Active = "overview"
1046 return p.executeRepo("repo/commit", w, params)
1047}
1048
1049type RepoTreeParams struct {
1050 BaseParams
1051 RepoInfo repoinfo.RepoInfo
1052 Active string
1053 BreadCrumbs [][]string
1054 Path string
1055 Raw bool
1056 HTMLReadme template.HTML
1057 EmailToDid map[string]string
1058 LastCommitInfo *types.LastCommitInfo
1059 Ref string
1060 Parent string
1061 DotDot string
1062 Files []types.NiceTree
1063 ReadmeFileName string
1064 Readme string
1065}
1066
1067type RepoTreeStats struct {
1068 NumFolders uint64
1069 NumFiles uint64
1070}
1071
1072func (r RepoTreeParams) TreeStats() RepoTreeStats {
1073 numFolders, numFiles := 0, 0
1074 for _, f := range r.Files {
1075 if !f.IsFile() {
1076 numFolders += 1
1077 } else if f.IsFile() {
1078 numFiles += 1
1079 }
1080 }
1081
1082 return RepoTreeStats{
1083 NumFolders: uint64(numFolders),
1084 NumFiles: uint64(numFiles),
1085 }
1086}
1087
1088func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error {
1089 params.Active = "overview"
1090
1091 rctx := p.rctx.Clone()
1092 rctx.RepoInfo = params.RepoInfo
1093 rctx.RepoInfo.Ref = params.Ref
1094 rctx.RendererType = markup.RendererTypeRepoMarkdown
1095
1096 if params.ReadmeFileName != "" {
1097 switch markup.GetFormat(params.ReadmeFileName) {
1098 case markup.FormatMarkdown:
1099 params.Raw = false
1100 htmlString := rctx.RenderMarkdown(params.Readme)
1101 sanitized := sanitizer.SanitizeDefault(htmlString)
1102 params.HTMLReadme = template.HTML(sanitized)
1103 default:
1104 params.Raw = true
1105 }
1106 }
1107
1108 return p.executeRepo("repo/tree", w, params)
1109}
1110
1111type RepoBranchesParams struct {
1112 BaseParams
1113 RepoInfo repoinfo.RepoInfo
1114 Active string
1115 types.RepoBranchesResponse
1116}
1117
1118func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error {
1119 params.Active = "overview"
1120 return p.executeRepo("repo/branches", w, params)
1121}
1122
1123type RepoTagsParams struct {
1124 BaseParams
1125 RepoInfo repoinfo.RepoInfo
1126 Active string
1127 types.RepoTagsResponse
1128 ArtifactMap map[plumbing.Hash][]models.Artifact
1129 DanglingArtifacts []models.Artifact
1130}
1131
1132func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error {
1133 params.Active = "overview"
1134 return p.executeRepo("repo/tags", w, params)
1135}
1136
1137type RepoTagParams struct {
1138 BaseParams
1139 RepoInfo repoinfo.RepoInfo
1140 Active string
1141 types.RepoTagResponse
1142 ArtifactMap map[plumbing.Hash][]models.Artifact
1143 DanglingArtifacts []models.Artifact
1144}
1145
1146func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error {
1147 params.Active = "overview"
1148 return p.executeRepo("repo/tag", w, params)
1149}
1150
1151type RepoArtifactParams struct {
1152 BaseParams
1153 RepoInfo repoinfo.RepoInfo
1154 Artifact models.Artifact
1155}
1156
1157func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error {
1158 return p.executePlain("repo/fragments/artifact", w, params)
1159}
1160
1161type RepoBlobParams struct {
1162 BaseParams
1163 RepoInfo repoinfo.RepoInfo
1164 Active string // always "overview"
1165 BreadCrumbs [][]string
1166 BlobView models.BlobView // TODO: expose this struct
1167 ShowRendered bool
1168 EmailToDid map[string]string
1169 LastCommitInfo *types.LastCommitInfo
1170 Ref string
1171 Path string
1172 Language string
1173}
1174
1175func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
1176 params.Active = "overview"
1177 return p.executeRepo("repo/blob", w, params)
1178}
1179
1180type Collaborator struct {
1181 Did string
1182 Role string
1183}
1184
1185type RepoGeneralSettingsParams struct {
1186 BaseParams
1187 RepoInfo repoinfo.RepoInfo
1188 Labels []models.LabelDefinition
1189 DefaultLabels []models.LabelDefinition
1190 SubscribedLabels map[string]struct{}
1191 ShouldSubscribeAll bool
1192 Active string
1193 Tab string
1194 Branches []types.Branch
1195}
1196
1197func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
1198 params.Active = "settings"
1199 params.Tab = "general"
1200 return p.executeRepo("repo/settings/general", w, params)
1201}
1202
1203type RepoAccessSettingsParams struct {
1204 BaseParams
1205 RepoInfo repoinfo.RepoInfo
1206 Active string
1207 Tab string
1208 Collaborators []Collaborator
1209 CanRemoveCollaborator bool
1210}
1211
1212func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
1213 params.Active = "settings"
1214 params.Tab = "access"
1215 return p.executeRepo("repo/settings/access", w, params)
1216}
1217
1218type RepoPipelineSettingsParams struct {
1219 BaseParams
1220 RepoInfo repoinfo.RepoInfo
1221 Active string
1222 Tab string
1223 Spindles []string
1224 CurrentSpindle string
1225 Secrets []map[string]any
1226}
1227
1228func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
1229 params.Active = "settings"
1230 params.Tab = "pipelines"
1231 return p.executeRepo("repo/settings/pipelines", w, params)
1232}
1233
1234type RepoWebhooksSettingsParams struct {
1235 BaseParams
1236 RepoInfo repoinfo.RepoInfo
1237 Active string
1238 Tab string
1239 Webhooks []models.Webhook
1240 WebhookDeliveries map[int64][]models.WebhookDelivery
1241}
1242
1243func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error {
1244 params.Active = "settings"
1245 params.Tab = "hooks"
1246 return p.executeRepo("repo/settings/hooks", w, params)
1247}
1248
1249type WebhookDeliveriesListParams struct {
1250 BaseParams
1251 RepoInfo repoinfo.RepoInfo
1252 Webhook *models.Webhook
1253 Deliveries []models.WebhookDelivery
1254}
1255
1256func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error {
1257 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries")
1258 if err != nil {
1259 return err
1260 }
1261 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params)
1262}
1263
1264type RepoSiteSettingsParams struct {
1265 BaseParams
1266 RepoInfo repoinfo.RepoInfo
1267 Active string
1268 Tab string
1269 Branches []types.Branch
1270 SiteConfig *models.RepoSite
1271 OwnerClaim *models.DomainClaim
1272 Deploys []models.SiteDeploy
1273 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or ""
1274}
1275
1276func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error {
1277 params.Active = "settings"
1278 params.Tab = "sites"
1279 return p.executeRepo("repo/settings/sites", w, params)
1280}
1281
1282type RepoIssuesParams struct {
1283 BaseParams
1284 RepoInfo repoinfo.RepoInfo
1285 Active string
1286 Issues []models.Issue
1287 IssueCount int
1288 LabelDefs map[string]*models.LabelDefinition
1289 Page pagination.Page
1290 FilterState string
1291 FilterQuery string
1292 BaseFilterQuery string
1293 VouchRelationships map[syntax.DID]*models.VouchRelationship
1294}
1295
1296func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error {
1297 params.Active = "issues"
1298 return p.executeRepo("repo/issues/issues", w, params)
1299}
1300
1301type RepoSingleIssueParams struct {
1302 BaseParams
1303 RepoInfo repoinfo.RepoInfo
1304 Active string
1305 Issue *models.Issue
1306 CommentList []models.CommentListItem
1307 Backlinks []models.RichReferenceLink
1308 LabelDefs map[string]*models.LabelDefinition
1309
1310 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1311 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1312 VouchRelationships map[syntax.DID]*models.VouchRelationship
1313
1314 // IsSubscribed is nil when the user is not logged in, true when subscribed,
1315 // false when explicitly unsubscribed, and nil when no explicit subscription.
1316 IsSubscribed *bool
1317}
1318
1319func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error {
1320 params.Active = "issues"
1321 return p.executeRepo("repo/issues/issue", w, params)
1322}
1323
1324type IssueSubscribeParams struct {
1325 RepoInfo repoinfo.RepoInfo
1326 IssueId int
1327 IsSubscribed *bool
1328}
1329
1330func (p *Pages) IssueSubscribeFragment(w io.Writer, params IssueSubscribeParams) error {
1331 return p.executePlain("repo/issues/fragments/subscribeButton", w, params)
1332}
1333
1334type PullSubscribeParams struct {
1335 RepoInfo repoinfo.RepoInfo
1336 PullId int
1337 IsSubscribed *bool
1338}
1339
1340func (p *Pages) PullSubscribeFragment(w io.Writer, params PullSubscribeParams) error {
1341 return p.executePlain("repo/pulls/fragments/subscribeButton", w, params)
1342}
1343
1344type EditIssueParams struct {
1345 BaseParams
1346 RepoInfo repoinfo.RepoInfo
1347 Issue *models.Issue
1348 Action string
1349}
1350
1351func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error {
1352 params.Action = "edit"
1353 return p.executePlain("repo/issues/fragments/putIssue", w, params)
1354}
1355
1356type ThreadReactionFragmentParams struct {
1357 Kind models.ReactionKind
1358 Count int
1359 Users []string
1360 IsReacted bool
1361 CommentRkey string
1362 SubjectUri string
1363}
1364
1365func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error {
1366 return p.executePlain("repo/fragments/reaction", w, params)
1367}
1368
1369type RepoNewIssueParams struct {
1370 BaseParams
1371 RepoInfo repoinfo.RepoInfo
1372 Issue *models.Issue // existing issue if any -- passed when editing
1373 Active string
1374 Action string
1375}
1376
1377func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
1378 params.Active = "issues"
1379 params.Action = "create"
1380 return p.executeRepo("repo/issues/new", w, params)
1381}
1382
1383type StackedDiff struct {
1384 Diff *types.NiceDiff
1385 Opts types.DiffOpts
1386}
1387
1388type RepoNewPullParams struct {
1389 BaseParams
1390 RepoInfo repoinfo.RepoInfo
1391 Branches []types.Branch
1392 SourceBranches []types.Branch
1393 ForkBranches []types.Branch
1394 Forks []models.Repo
1395 Source Source
1396 SourceBranch string
1397 TargetBranch string
1398 Fork string
1399 Patch string
1400 Title string
1401 Body string
1402 TitleDirty bool
1403 BodyDirty bool
1404 IsStacked bool
1405 Comparison *types.RepoFormatPatchResponse
1406 Diff *types.NiceDiff
1407 DiffOpts types.DiffOpts
1408 StackedDiffs []StackedDiff
1409 MergeCheck *types.MergeCheckResponse
1410 StackTitles map[string]string
1411 StackBodies map[string]string
1412 PrefillError string
1413 Active string
1414 LabelDefs map[string]*models.LabelDefinition
1415 LabelState models.LabelState
1416 StackLabelStates map[string]models.LabelState
1417}
1418
1419func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
1420 params.Active = "pulls"
1421 return p.executeRepo("repo/pulls/new", w, params)
1422}
1423
1424func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error {
1425 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params)
1426}
1427
1428func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error {
1429 return p.executePlain("fragments/markdownPreview", w, body)
1430}
1431
1432type EditPullParams struct {
1433 LoggedInUser *oauth.MultiAccountUser
1434 RepoInfo repoinfo.RepoInfo
1435 Pull *models.Pull
1436}
1437
1438func (p *Pages) EditPullFragment(w io.Writer, params EditPullParams) error {
1439 return p.executePlain("repo/pulls/fragments/pullEdit", w, params)
1440}
1441
1442type RepoPullsParams struct {
1443 BaseParams
1444 RepoInfo repoinfo.RepoInfo
1445 Pulls []*models.Pull
1446 Active string
1447 FilterState string
1448 FilterQuery string
1449 BaseFilterQuery string
1450 Stacks []models.Stack
1451 Pipelines map[string]types.Pipeline
1452 LabelDefs map[string]*models.LabelDefinition
1453 Page pagination.Page
1454 PullCount int
1455 VouchRelationships map[syntax.DID]*models.VouchRelationship
1456}
1457
1458func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
1459 params.Active = "pulls"
1460 return p.executeRepo("repo/pulls/pulls", w, params)
1461}
1462
1463type ResubmitResult uint64
1464
1465const (
1466 ShouldResubmit ResubmitResult = iota
1467 ShouldNotResubmit
1468 Unknown
1469)
1470
1471func (r ResubmitResult) Yes() bool {
1472 return r == ShouldResubmit
1473}
1474func (r ResubmitResult) No() bool {
1475 return r == ShouldNotResubmit
1476}
1477func (r ResubmitResult) Unknown() bool {
1478 return r == Unknown
1479}
1480
1481type RepoSinglePullParams struct {
1482 BaseParams
1483 RepoInfo repoinfo.RepoInfo
1484 Active string
1485 Pull *models.Pull
1486 Stack models.Stack
1487 Backlinks []models.RichReferenceLink
1488 BranchDeleteStatus *models.BranchDeleteStatus
1489 MergeCheck types.MergeCheckResponse
1490 ResubmitCheck ResubmitResult
1491 Pipelines map[string]types.Pipeline
1492 Diff types.DiffRenderer
1493 DiffOpts types.DiffOpts
1494 ActiveRound int
1495 IsInterdiff bool
1496
1497 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1498 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1499
1500 LabelDefs map[string]*models.LabelDefinition
1501 VouchRelationships map[syntax.DID]*models.VouchRelationship
1502 VouchSkips map[syntax.DID]bool
1503
1504 // IsSubscribed is nil when not logged in, true when subscribed, false when explicitly unsubscribed.
1505 IsSubscribed *bool
1506}
1507
1508func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1509 params.Active = "pulls"
1510 return p.executeRepo("repo/pulls/pull", w, params)
1511}
1512
1513type PullResubmitParams struct {
1514 BaseParams
1515 RepoInfo repoinfo.RepoInfo
1516 Pull *models.Pull
1517 SubmissionId int
1518}
1519
1520func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1521 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1522}
1523
1524type PullActionsParams struct {
1525 BaseParams
1526 RepoInfo repoinfo.RepoInfo
1527 Pull *models.Pull
1528 RoundNumber int
1529 MergeCheck types.MergeCheckResponse
1530 ResubmitCheck ResubmitResult
1531 BranchDeleteStatus *models.BranchDeleteStatus
1532 Stack models.Stack
1533
1534 // Workflow warning state for fork-based pulls without a pipeline on the
1535 // latest commit. WorkflowsChanged and ChangedWorkflowFiles are computed
1536 // from the latest round's patch.
1537 WorkflowsChanged bool
1538 ChangedWorkflowFiles []string
1539 HasPipeline bool
1540
1541 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1542 // that fetches the real, checked fragment
1543 Loading bool
1544}
1545
1546func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1547 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1548}
1549
1550type RepoCompareParams struct {
1551 BaseParams
1552 RepoInfo repoinfo.RepoInfo
1553 Forks []models.Repo
1554 Branches []types.Branch
1555 Tags []*types.TagReference
1556 Base string
1557 Head string
1558 Diff *types.NiceDiff
1559 DiffOpts types.DiffOpts
1560
1561 Active string
1562}
1563
1564func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1565 params.Active = "overview"
1566 return p.executeRepo("repo/compare/compare", w, params)
1567}
1568
1569type RepoCompareNewParams struct {
1570 BaseParams
1571 RepoInfo repoinfo.RepoInfo
1572 Forks []models.Repo
1573 Branches []types.Branch
1574 Tags []*types.TagReference
1575 Base string
1576 Head string
1577
1578 Active string
1579}
1580
1581func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1582 params.Active = "overview"
1583 return p.executeRepo("repo/compare/new", w, params)
1584}
1585
1586type RepoCompareAllowPullParams struct {
1587 BaseParams
1588 RepoInfo repoinfo.RepoInfo
1589 Base string
1590 Head string
1591}
1592
1593func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1594 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1595}
1596
1597type RepoCompareDiffFragmentParams struct {
1598 Diff types.NiceDiff
1599 DiffOpts types.DiffOpts
1600}
1601
1602func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1603 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1604}
1605
1606type LabelPanelParams struct {
1607 BaseParams
1608 RepoInfo repoinfo.RepoInfo
1609 Defs map[string]*models.LabelDefinition
1610 Subject string
1611 State models.LabelState
1612}
1613
1614func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1615 return p.executePlain("repo/fragments/labelPanel", w, params)
1616}
1617
1618type EditLabelPanelParams struct {
1619 BaseParams
1620 RepoInfo repoinfo.RepoInfo
1621 Defs map[string]*models.LabelDefinition
1622 Subject string
1623 State models.LabelState
1624 Prefix string
1625}
1626
1627func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1628 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1629}
1630
1631type RepoStarsParams struct {
1632 BaseParams
1633 RepoInfo repoinfo.RepoInfo
1634 Active string
1635 Starrers []models.Star
1636 Page pagination.Page
1637 TotalCount int
1638}
1639
1640func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1641 params.Active = "overview"
1642 return p.executeRepo("repo/stars", w, params)
1643}
1644
1645type RepoForksParams struct {
1646 BaseParams
1647 RepoInfo repoinfo.RepoInfo
1648 Active string
1649 Forks []models.Repo
1650 Page pagination.Page
1651 TotalCount int
1652}
1653
1654func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1655 params.Active = "overview"
1656 return p.executeRepo("repo/forks", w, params)
1657}
1658
1659type PipelinesParams struct {
1660 BaseParams
1661 RepoInfo repoinfo.RepoInfo
1662 Pipelines []types.Pipeline
1663 Active string
1664 FilterKind string
1665 Total int64
1666}
1667
1668func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1669 params.Active = "pipelines"
1670 return p.executeRepo("repo/pipelines/pipelines", w, params)
1671}
1672
1673type LogBlockParams struct {
1674 Id int
1675 Name string
1676 Command string
1677 Collapsed bool
1678 StartTime time.Time
1679}
1680
1681func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1682 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1683}
1684
1685type LogBlockEndParams struct {
1686 Id int
1687 StartTime time.Time
1688 EndTime time.Time
1689}
1690
1691func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1692 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1693}
1694
1695type LogLineParams struct {
1696 Id int
1697 Content template.HTML
1698}
1699
1700func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1701 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1702}
1703
1704type WorkflowSymbolOOBParams struct {
1705 Name string
1706 Statuses models.WorkflowStatus
1707}
1708
1709func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1710 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1711}
1712
1713type PipelineStatusesParams struct {
1714 RepoInfo repoinfo.RepoInfo
1715 Pipelines map[string]types.Pipeline
1716}
1717
1718func (p *Pages) PipelineStatusesFragment(w io.Writer, params PipelineStatusesParams) error {
1719 return p.executePlain("repo/fragments/pipelineStatuses", w, params)
1720}
1721
1722type WorkflowParams struct {
1723 BaseParams
1724 RepoInfo repoinfo.RepoInfo
1725 Pipeline types.Pipeline
1726 Workflow string
1727 SSHLogCommand string
1728 Active string
1729}
1730
1731func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1732 params.Active = "pipelines"
1733 return p.executeRepo("repo/pipelines/workflow", w, params)
1734}
1735
1736type PutStringParams struct {
1737 BaseParams
1738 Action string
1739
1740 // this is supplied in the case of editing an existing string
1741 String models.String
1742}
1743
1744func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1745 return p.execute("strings/put", w, params)
1746}
1747
1748type StringsDashboardParams struct {
1749 BaseParams
1750 Card ProfileCard
1751 Strings []models.String
1752}
1753
1754func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1755 return p.execute("strings/dashboard", w, params)
1756}
1757
1758type StringTimelineParams struct {
1759 BaseParams
1760 Strings []models.String
1761}
1762
1763func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1764 return p.execute("strings/timeline", w, params)
1765}
1766
1767type SingleStringParams struct {
1768 BaseParams
1769 ShowRendered bool
1770 RenderToggle bool
1771 RenderedContents template.HTML
1772 String *models.String
1773 Stats models.StringStats
1774 IsStarred bool
1775 StarCount int
1776 Owner identity.Identity
1777 CommentList []models.CommentListItem
1778
1779 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1780 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1781 VouchRelationships map[syntax.DID]*models.VouchRelationship
1782}
1783
1784func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1785 return p.execute("strings/string", w, params)
1786}
1787
1788type SearchReposParams struct {
1789 BaseParams
1790 FilterType string // "repo" | "code"
1791 Repos []SearchResult
1792 Page pagination.Page
1793 ResultCount int
1794 FilterQuery string
1795 SortParam string
1796 TimeTaken time.Duration
1797 DocCount int64
1798 ErrorMsg string
1799}
1800
1801func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1802 params.FilterType = "repo"
1803 return p.execute("search/search", w, params)
1804}
1805
1806type SearchQuickParams struct {
1807 Repos []models.Repo
1808 Query string
1809 Total int
1810}
1811
1812func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1813 return p.executePlain("search/fragments/quick", w, params)
1814}
1815
1816func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1817 tpl, err := p.parse("search/fragments/quick")
1818 if err != nil {
1819 return err
1820 }
1821 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1822}
1823
1824type SearchResult struct {
1825 RepoDID syntax.DID
1826 Repo *models.Repo
1827 FilePath string
1828 Branches []string
1829 Commit string
1830 Language string
1831
1832 File *CodeSearchResult_File // filename match
1833 Chunks CodeSearchResult_Chunks // content matches
1834}
1835
1836// CodeSearchResult_Chunk is a content match with its lines pre-rendered.
1837type CodeSearchResult_Chunk struct {
1838 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges
1839 MatchCount int // number of match ranges in this chunk
1840}
1841
1842type CodeSearchResult_Chunks []CodeSearchResult_Chunk
1843
1844func (cs CodeSearchResult_Chunks) MatchCount() int {
1845 count := 0
1846 for _, c := range cs {
1847 count += c.MatchCount
1848 }
1849 return count
1850}
1851
1852type CodeSearchResult_File struct {
1853 NameSpans []ChunkSpan // precomputed from FilePath/Ranges
1854}
1855
1856type ChunkSpan struct {
1857 Text string
1858 Match bool
1859}
1860
1861type ChunkLine struct {
1862 Num int
1863 Spans []ChunkSpan
1864 Highlight bool
1865}
1866
1867// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each
1868// line into matched/unmatched spans using ranges. startLine is the 1-based line
1869// number of the first line.
1870func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine {
1871 if startLine < 1 {
1872 startLine = 1
1873 }
1874 // trim a single trailing newline so we don't emit a spurious empty line
1875 content = strings.TrimSuffix(content, "\n")
1876 lines := strings.Split(content, "\n")
1877 out := make([]ChunkLine, len(lines))
1878 for i, text := range lines {
1879 num := startLine + i
1880 runes := []rune(text)
1881
1882 // collect matched rune intervals [c0,c1) for this line
1883 var intervals [][2]int
1884 for _, rg := range ranges {
1885 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) {
1886 continue
1887 }
1888 c0, c1 := 0, len(runes)
1889 if num == int(rg.Start.LineNumber) {
1890 c0 = int(rg.Start.Column) - 1
1891 }
1892 if num == int(rg.End.LineNumber) {
1893 c1 = int(rg.End.Column) - 1
1894 }
1895 c0 = max(0, min(c0, len(runes)))
1896 c1 = max(0, min(c1, len(runes)))
1897 if c0 < c1 {
1898 intervals = append(intervals, [2]int{c0, c1})
1899 }
1900 }
1901 intervals = mergeIntervals(intervals)
1902
1903 out[i] = ChunkLine{
1904 Num: num,
1905 Spans: spanRunes(runes, intervals),
1906 Highlight: len(intervals) > 0,
1907 }
1908 }
1909 return out
1910}
1911
1912// FileNameSpans splits a filename into matched/unmatched spans using ranges.
1913// Filename ranges live on line 1; columns are clamped to rune bounds.
1914func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan {
1915 runes := []rune(name)
1916 var intervals [][2]int
1917 for _, rg := range ranges {
1918 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 {
1919 continue
1920 }
1921 c0 := max(0, min(int(rg.Start.Column)-1, len(runes)))
1922 c1 := max(0, min(int(rg.End.Column)-1, len(runes)))
1923 if c0 < c1 {
1924 intervals = append(intervals, [2]int{c0, c1})
1925 }
1926 }
1927 return spanRunes(runes, mergeIntervals(intervals))
1928}
1929
1930type CodeSearchParams struct {
1931 BaseParams
1932 FilterType string // "code"
1933 FilterQuery string
1934 Results []SearchResult
1935 Page pagination.Page
1936 HasMore bool
1937 ErrorMsg string
1938
1939 MatchCount int
1940 FileCount int
1941 TimeTaken time.Duration
1942}
1943
1944func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error {
1945 params.FilterType = "code"
1946 return p.execute("search/search", w, params)
1947}
1948
1949func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1950 return p.execute("timeline/home", w, params)
1951}
1952
1953type CommentBodyFragmentParams struct {
1954 Comment models.Comment
1955 Reactions map[models.ReactionKind]models.ReactionDisplayData
1956 UserReacted map[models.ReactionKind]bool
1957}
1958
1959func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1960 return p.executePlain("fragments/comment/commentBody", w, params)
1961}
1962
1963type PullCommentFragmentParams struct {
1964 LoggedInUser *oauth.MultiAccountUser
1965 Comment models.Comment
1966 Reactions map[models.ReactionKind]models.ReactionDisplayData
1967 UserReacted map[models.ReactionKind]bool
1968 HxSwapOob bool
1969}
1970
1971func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error {
1972 return p.executePlain("fragments/comment/pullComment", w, params)
1973}
1974
1975type CommentHeaderFragmentParams struct {
1976 Comment models.Comment
1977 Reactions map[models.ReactionKind]models.ReactionDisplayData
1978 UserReacted map[models.ReactionKind]bool
1979 HxSwapOob bool
1980}
1981
1982func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1983 return p.executePlain("fragments/comment/commentHeader", w, params)
1984}
1985
1986type EditCommentFragmentParams struct {
1987 Comment models.Comment
1988}
1989
1990func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1991 return p.executePlain("fragments/comment/edit", w, params)
1992}
1993
1994type ReplyCommentFragmentParams struct {
1995 BaseParams
1996}
1997
1998func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1999 return p.executePlain("fragments/comment/reply", w, params)
2000}
2001
2002type ReplyPlaceholderFragmentParams struct {
2003 BaseParams
2004}
2005
2006func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
2007 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
2008}
2009
2010func (p *Pages) Static() http.Handler {
2011 if p.dev {
2012 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
2013 }
2014
2015 sub, err := fs.Sub(p.embedFS, "static")
2016 if err != nil {
2017 p.logger.Error("no static dir found? that's crazy", "err", err)
2018 panic(err)
2019 }
2020 // Custom handler to apply Cache-Control headers for font files
2021 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
2022}
2023
2024func Cache(h http.Handler) http.Handler {
2025 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2026 path := strings.Split(r.URL.Path, "?")[0]
2027
2028 if strings.HasSuffix(path, ".css") {
2029 // on day for css files
2030 w.Header().Set("Cache-Control", "public, max-age=86400")
2031 } else {
2032 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
2033 }
2034 h.ServeHTTP(w, r)
2035 })
2036}
2037
2038func (p *Pages) CssContentHash() string {
2039 cssFile, err := p.embedFS.Open("static/tw.css")
2040 if err != nil {
2041 slog.Debug("Error opening CSS file", "err", err)
2042 return ""
2043 }
2044 defer cssFile.Close()
2045
2046 hasher := sha256.New()
2047 if _, err := io.Copy(hasher, cssFile); err != nil {
2048 slog.Debug("Error hashing CSS file", "err", err)
2049 return ""
2050 }
2051
2052 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
2053}
2054
2055func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
2056 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
2057}
2058
2059func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
2060 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
2061}
2062
2063func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
2064 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
2065}
2066
2067func (p *Pages) Error500(w io.Writer) error {
2068 return p.execute("errors/500", w, BaseParams{})
2069}
2070
2071func (p *Pages) Error404(w io.Writer) error {
2072 return p.execute("errors/404", w, BaseParams{})
2073}
2074
2075func (p *Pages) ErrorKnot404(w io.Writer) error {
2076 return p.execute("errors/knot404", w, BaseParams{})
2077}
2078
2079func (p *Pages) Error503(w io.Writer) error {
2080 return p.execute("errors/503", w, BaseParams{})
2081}