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