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 TitleDirty bool
1316 BodyDirty bool
1317 IsStacked bool
1318 Comparison *types.RepoFormatPatchResponse
1319 Diff *types.NiceDiff
1320 DiffOpts types.DiffOpts
1321 StackedDiffs []StackedDiff
1322 MergeCheck *types.MergeCheckResponse
1323 StackTitles map[string]string
1324 StackBodies map[string]string
1325 PrefillError string
1326 Active string
1327 LabelDefs map[string]*models.LabelDefinition
1328 LabelState models.LabelState
1329 StackLabelStates map[string]models.LabelState
1330}
1331
1332func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
1333 params.Active = "pulls"
1334 return p.executeRepo("repo/pulls/new", w, params)
1335}
1336
1337func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error {
1338 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params)
1339}
1340
1341func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error {
1342 return p.executePlain("fragments/markdownPreview", w, body)
1343}
1344
1345type RepoPullsParams struct {
1346 BaseParams
1347 RepoInfo repoinfo.RepoInfo
1348 Pulls []*models.Pull
1349 Active string
1350 FilterState string
1351 FilterQuery string
1352 BaseFilterQuery string
1353 Stacks []models.Stack
1354 Pipelines map[string]models.Pipeline
1355 LabelDefs map[string]*models.LabelDefinition
1356 Page pagination.Page
1357 PullCount int
1358 VouchRelationships map[syntax.DID]*models.VouchRelationship
1359}
1360
1361func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
1362 params.Active = "pulls"
1363 return p.executeRepo("repo/pulls/pulls", w, params)
1364}
1365
1366type ResubmitResult uint64
1367
1368const (
1369 ShouldResubmit ResubmitResult = iota
1370 ShouldNotResubmit
1371 Unknown
1372)
1373
1374func (r ResubmitResult) Yes() bool {
1375 return r == ShouldResubmit
1376}
1377func (r ResubmitResult) No() bool {
1378 return r == ShouldNotResubmit
1379}
1380func (r ResubmitResult) Unknown() bool {
1381 return r == Unknown
1382}
1383
1384type RepoSinglePullParams struct {
1385 BaseParams
1386 RepoInfo repoinfo.RepoInfo
1387 Active string
1388 Pull *models.Pull
1389 Stack models.Stack
1390 Backlinks []models.RichReferenceLink
1391 BranchDeleteStatus *models.BranchDeleteStatus
1392 MergeCheck types.MergeCheckResponse
1393 ResubmitCheck ResubmitResult
1394 Pipelines map[string]models.Pipeline
1395 Diff types.DiffRenderer
1396 DiffOpts types.DiffOpts
1397 ActiveRound int
1398 IsInterdiff bool
1399
1400 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1401 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1402
1403 LabelDefs map[string]*models.LabelDefinition
1404 VouchRelationships map[syntax.DID]*models.VouchRelationship
1405 VouchSkips map[syntax.DID]bool
1406}
1407
1408func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
1409 params.Active = "pulls"
1410 return p.executeRepo("repo/pulls/pull", w, params)
1411}
1412
1413type PullResubmitParams struct {
1414 BaseParams
1415 RepoInfo repoinfo.RepoInfo
1416 Pull *models.Pull
1417 SubmissionId int
1418}
1419
1420func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error {
1421 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params)
1422}
1423
1424type PullActionsParams struct {
1425 BaseParams
1426 RepoInfo repoinfo.RepoInfo
1427 Pull *models.Pull
1428 RoundNumber int
1429 MergeCheck types.MergeCheckResponse
1430 ResubmitCheck ResubmitResult
1431 BranchDeleteStatus *models.BranchDeleteStatus
1432 Stack models.Stack
1433
1434 // renders buttons in a pre-check state and attaches the hx-trigger="load"
1435 // that fetches the real, checked fragment
1436 Loading bool
1437}
1438
1439func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
1440 return p.executePlain("repo/pulls/fragments/pullActions", w, params)
1441}
1442
1443type PullNewCommentParams struct {
1444 BaseParams
1445 RepoInfo repoinfo.RepoInfo
1446 Pull *models.Pull
1447 RoundNumber int
1448}
1449
1450func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error {
1451 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params)
1452}
1453
1454type RepoCompareParams struct {
1455 BaseParams
1456 RepoInfo repoinfo.RepoInfo
1457 Forks []models.Repo
1458 Branches []types.Branch
1459 Tags []*types.TagReference
1460 Base string
1461 Head string
1462 Diff *types.NiceDiff
1463 DiffOpts types.DiffOpts
1464
1465 Active string
1466}
1467
1468func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error {
1469 params.Active = "overview"
1470 return p.executeRepo("repo/compare/compare", w, params)
1471}
1472
1473type RepoCompareNewParams struct {
1474 BaseParams
1475 RepoInfo repoinfo.RepoInfo
1476 Forks []models.Repo
1477 Branches []types.Branch
1478 Tags []*types.TagReference
1479 Base string
1480 Head string
1481
1482 Active string
1483}
1484
1485func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error {
1486 params.Active = "overview"
1487 return p.executeRepo("repo/compare/new", w, params)
1488}
1489
1490type RepoCompareAllowPullParams struct {
1491 BaseParams
1492 RepoInfo repoinfo.RepoInfo
1493 Base string
1494 Head string
1495}
1496
1497func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error {
1498 return p.executePlain("repo/fragments/compareAllowPull", w, params)
1499}
1500
1501type RepoCompareDiffFragmentParams struct {
1502 Diff types.NiceDiff
1503 DiffOpts types.DiffOpts
1504}
1505
1506func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1507 return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1508}
1509
1510type LabelPanelParams struct {
1511 BaseParams
1512 RepoInfo repoinfo.RepoInfo
1513 Defs map[string]*models.LabelDefinition
1514 Subject string
1515 State models.LabelState
1516}
1517
1518func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error {
1519 return p.executePlain("repo/fragments/labelPanel", w, params)
1520}
1521
1522type EditLabelPanelParams struct {
1523 BaseParams
1524 RepoInfo repoinfo.RepoInfo
1525 Defs map[string]*models.LabelDefinition
1526 Subject string
1527 State models.LabelState
1528 Prefix string
1529}
1530
1531func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error {
1532 return p.executePlain("repo/fragments/editLabelPanel", w, params)
1533}
1534
1535type RepoStarsParams struct {
1536 BaseParams
1537 RepoInfo repoinfo.RepoInfo
1538 Active string
1539 Starrers []models.Star
1540 Page pagination.Page
1541 TotalCount int
1542}
1543
1544func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error {
1545 params.Active = "overview"
1546 return p.executeRepo("repo/stars", w, params)
1547}
1548
1549type RepoForksParams struct {
1550 BaseParams
1551 RepoInfo repoinfo.RepoInfo
1552 Active string
1553 Forks []models.Repo
1554 Page pagination.Page
1555 TotalCount int
1556}
1557
1558func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error {
1559 params.Active = "overview"
1560 return p.executeRepo("repo/forks", w, params)
1561}
1562
1563type PipelinesParams struct {
1564 BaseParams
1565 RepoInfo repoinfo.RepoInfo
1566 Pipelines []models.Pipeline
1567 Active string
1568 FilterKind string
1569 Total int64
1570}
1571
1572func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
1573 params.Active = "pipelines"
1574 return p.executeRepo("repo/pipelines/pipelines", w, params)
1575}
1576
1577type LogBlockParams struct {
1578 Id int
1579 Name string
1580 Command string
1581 Collapsed bool
1582 StartTime time.Time
1583}
1584
1585func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error {
1586 return p.executePlain("repo/pipelines/fragments/logBlock", w, params)
1587}
1588
1589type LogBlockEndParams struct {
1590 Id int
1591 StartTime time.Time
1592 EndTime time.Time
1593}
1594
1595func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error {
1596 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params)
1597}
1598
1599type LogLineParams struct {
1600 Id int
1601 Content template.HTML
1602}
1603
1604func (p *Pages) LogLine(w io.Writer, params LogLineParams) error {
1605 return p.executePlain("repo/pipelines/fragments/logLine", w, params)
1606}
1607
1608type WorkflowSymbolOOBParams struct {
1609 Name string
1610 Statuses models.WorkflowStatus
1611}
1612
1613func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error {
1614 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params)
1615}
1616
1617type WorkflowParams struct {
1618 BaseParams
1619 RepoInfo repoinfo.RepoInfo
1620 Pipeline models.Pipeline
1621 Workflow string
1622 LogUrl string
1623 Active string
1624}
1625
1626func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error {
1627 params.Active = "pipelines"
1628 return p.executeRepo("repo/pipelines/workflow", w, params)
1629}
1630
1631type PutStringParams struct {
1632 BaseParams
1633 Action string
1634
1635 // this is supplied in the case of editing an existing string
1636 String models.String
1637}
1638
1639func (p *Pages) PutString(w io.Writer, params PutStringParams) error {
1640 return p.execute("strings/put", w, params)
1641}
1642
1643type StringsDashboardParams struct {
1644 BaseParams
1645 Card ProfileCard
1646 Strings []models.String
1647}
1648
1649func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error {
1650 return p.execute("strings/dashboard", w, params)
1651}
1652
1653type StringTimelineParams struct {
1654 BaseParams
1655 Strings []models.String
1656}
1657
1658func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error {
1659 return p.execute("strings/timeline", w, params)
1660}
1661
1662type SingleStringParams struct {
1663 BaseParams
1664 ShowRendered bool
1665 RenderToggle bool
1666 RenderedContents template.HTML
1667 String *models.String
1668 Stats models.StringStats
1669 IsStarred bool
1670 StarCount int
1671 Owner identity.Identity
1672 CommentList []models.CommentListItem
1673
1674 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData
1675 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool
1676 VouchRelationships map[syntax.DID]*models.VouchRelationship
1677}
1678
1679func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error {
1680 return p.execute("strings/string", w, params)
1681}
1682
1683type SearchReposParams struct {
1684 BaseParams
1685 Repos []models.Repo
1686 Page pagination.Page
1687 ResultCount int
1688 FilterQuery string
1689 SortParam string
1690 TimeTaken time.Duration
1691 DocCount int64
1692}
1693
1694func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error {
1695 return p.execute("search/search", w, params)
1696}
1697
1698type SearchQuickParams struct {
1699 Repos []models.Repo
1700 Query string
1701 Total int
1702}
1703
1704func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error {
1705 return p.executePlain("search/fragments/quick", w, params)
1706}
1707
1708func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error {
1709 tpl, err := p.parse("search/fragments/quick")
1710 if err != nil {
1711 return err
1712 }
1713 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params)
1714}
1715
1716func (p *Pages) Home(w io.Writer, params TimelineParams) error {
1717 return p.execute("timeline/home", w, params)
1718}
1719
1720type CommentBodyFragmentParams struct {
1721 Comment models.Comment
1722 Reactions map[models.ReactionKind]models.ReactionDisplayData
1723 UserReacted map[models.ReactionKind]bool
1724}
1725
1726func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
1727 return p.executePlain("fragments/comment/commentBody", w, params)
1728}
1729
1730type CommentHeaderFragmentParams struct {
1731 Comment models.Comment
1732 Reactions map[models.ReactionKind]models.ReactionDisplayData
1733 UserReacted map[models.ReactionKind]bool
1734 HxSwapOob bool
1735}
1736
1737func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error {
1738 return p.executePlain("fragments/comment/commentHeader", w, params)
1739}
1740
1741type EditCommentFragmentParams struct {
1742 Comment models.Comment
1743}
1744
1745func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
1746 return p.executePlain("fragments/comment/edit", w, params)
1747}
1748
1749type ReplyCommentFragmentParams struct {
1750 BaseParams
1751}
1752
1753func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
1754 return p.executePlain("fragments/comment/reply", w, params)
1755}
1756
1757type ReplyPlaceholderFragmentParams struct {
1758 BaseParams
1759}
1760
1761func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
1762 return p.executePlain("fragments/comment/replyPlaceholder", w, params)
1763}
1764
1765func (p *Pages) Static() http.Handler {
1766 if p.dev {
1767 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
1768 }
1769
1770 sub, err := fs.Sub(p.embedFS, "static")
1771 if err != nil {
1772 p.logger.Error("no static dir found? that's crazy", "err", err)
1773 panic(err)
1774 }
1775 // Custom handler to apply Cache-Control headers for font files
1776 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
1777}
1778
1779func Cache(h http.Handler) http.Handler {
1780 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1781 path := strings.Split(r.URL.Path, "?")[0]
1782
1783 if strings.HasSuffix(path, ".css") {
1784 // on day for css files
1785 w.Header().Set("Cache-Control", "public, max-age=86400")
1786 } else {
1787 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
1788 }
1789 h.ServeHTTP(w, r)
1790 })
1791}
1792
1793func (p *Pages) CssContentHash() string {
1794 cssFile, err := p.embedFS.Open("static/tw.css")
1795 if err != nil {
1796 slog.Debug("Error opening CSS file", "err", err)
1797 return ""
1798 }
1799 defer cssFile.Close()
1800
1801 hasher := sha256.New()
1802 if _, err := io.Copy(hasher, cssFile); err != nil {
1803 slog.Debug("Error hashing CSS file", "err", err)
1804 return ""
1805 }
1806
1807 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash
1808}
1809
1810func (p *Pages) DangerPasswordTokenStep(w io.Writer) error {
1811 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil)
1812}
1813
1814func (p *Pages) DangerPasswordSuccess(w io.Writer) error {
1815 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil)
1816}
1817
1818func (p *Pages) DangerDeleteTokenStep(w io.Writer) error {
1819 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil)
1820}
1821
1822func (p *Pages) Error500(w io.Writer) error {
1823 return p.execute("errors/500", w, nil)
1824}
1825
1826func (p *Pages) Error404(w io.Writer) error {
1827 return p.execute("errors/404", w, nil)
1828}
1829
1830func (p *Pages) ErrorKnot404(w io.Writer) error {
1831 return p.execute("errors/knot404", w, nil)
1832}
1833
1834func (p *Pages) Error503(w io.Writer) error {
1835 return p.execute("errors/503", w, nil)
1836}