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