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