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