Monorepo for Tangled
at master 1590 lines 42 kB view raw
1package pages 2 3import ( 4 "crypto/sha256" 5 "embed" 6 "encoding/hex" 7 "fmt" 8 "html/template" 9 "io" 10 "io/fs" 11 "log/slog" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 19 "tangled.org/core/api/tangled" 20 "tangled.org/core/appview/commitverify" 21 "tangled.org/core/appview/config" 22 "tangled.org/core/appview/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/oauth" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/pages/repoinfo" 27 "tangled.org/core/appview/pagination" 28 "tangled.org/core/idresolver" 29 "tangled.org/core/patchutil" 30 "tangled.org/core/types" 31 32 "github.com/bluesky-social/indigo/atproto/identity" 33 "github.com/bluesky-social/indigo/atproto/syntax" 34 "github.com/go-git/go-git/v5/plumbing" 35) 36 37//go:embed templates/* static legal 38var Files embed.FS 39 40type Pages struct { 41 mu sync.RWMutex 42 cache *TmplCache[string, *template.Template] 43 44 avatar config.AvatarConfig 45 resolver *idresolver.Resolver 46 db *db.DB 47 dev bool 48 embedFS fs.FS 49 templateDir string // Path to templates on disk for dev mode 50 rctx *markup.RenderContext 51 logger *slog.Logger 52} 53 54func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages { 55 // initialized with safe defaults, can be overriden per use 56 rctx := &markup.RenderContext{ 57 IsDev: config.Core.Dev, 58 Hostname: config.Core.AppviewHost, 59 CamoUrl: config.Camo.Host, 60 CamoSecret: config.Camo.SharedSecret, 61 Sanitizer: markup.NewSanitizer(), 62 Files: Files, 63 } 64 65 p := &Pages{ 66 mu: sync.RWMutex{}, 67 cache: NewTmplCache[string, *template.Template](), 68 dev: config.Core.Dev, 69 avatar: config.Avatar, 70 rctx: rctx, 71 resolver: res, 72 db: database, 73 templateDir: "appview/pages", 74 logger: logger, 75 } 76 77 if p.dev { 78 p.embedFS = os.DirFS(p.templateDir) 79 } else { 80 p.embedFS = Files 81 } 82 83 return p 84} 85 86// reverse of pathToName 87func (p *Pages) nameToPath(s string) string { 88 return "templates/" + s + ".html" 89} 90 91func (p *Pages) fragmentPaths() ([]string, error) { 92 var fragmentPaths []string 93 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 94 if err != nil { 95 return err 96 } 97 if d.IsDir() { 98 return nil 99 } 100 if !strings.HasSuffix(path, ".html") { 101 return nil 102 } 103 if !strings.Contains(path, "fragments/") { 104 return nil 105 } 106 fragmentPaths = append(fragmentPaths, path) 107 return nil 108 }) 109 if err != nil { 110 return nil, err 111 } 112 113 return fragmentPaths, nil 114} 115 116// parse without memoization 117func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 118 paths, err := p.fragmentPaths() 119 if err != nil { 120 return nil, err 121 } 122 for _, s := range stack { 123 paths = append(paths, p.nameToPath(s)) 124 } 125 126 funcs := p.funcMap() 127 top := stack[len(stack)-1] 128 parsed, err := template.New(top). 129 Funcs(funcs). 130 ParseFS(p.embedFS, paths...) 131 if err != nil { 132 return nil, err 133 } 134 135 return parsed, nil 136} 137 138func (p *Pages) parse(stack ...string) (*template.Template, error) { 139 key := strings.Join(stack, "|") 140 141 // never cache in dev mode 142 if cached, exists := p.cache.Get(key); !p.dev && exists { 143 return cached, nil 144 } 145 146 result, err := p.rawParse(stack...) 147 if err != nil { 148 return nil, err 149 } 150 151 p.cache.Set(key, result) 152 return result, nil 153} 154 155func (p *Pages) parseBase(top string) (*template.Template, error) { 156 stack := []string{ 157 "layouts/base", 158 top, 159 } 160 return p.parse(stack...) 161} 162 163func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 164 stack := []string{ 165 "layouts/base", 166 "layouts/repobase", 167 top, 168 } 169 return p.parse(stack...) 170} 171 172func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 173 stack := []string{ 174 "layouts/base", 175 "layouts/profilebase", 176 top, 177 } 178 return p.parse(stack...) 179} 180 181func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 182 stack := []string{ 183 "layouts/base", 184 "layouts/loginbase", 185 top, 186 } 187 return p.parse(stack...) 188} 189 190func (p *Pages) executePlain(name string, w io.Writer, params any) error { 191 tpl, err := p.parse(name) 192 if err != nil { 193 return err 194 } 195 196 return tpl.Execute(w, params) 197} 198 199func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 200 tpl, err := p.parseLoginBase(name) 201 if err != nil { 202 return err 203 } 204 205 return tpl.ExecuteTemplate(w, "layouts/base", params) 206} 207 208func (p *Pages) execute(name string, w io.Writer, params any) error { 209 tpl, err := p.parseBase(name) 210 if err != nil { 211 return err 212 } 213 214 return tpl.ExecuteTemplate(w, "layouts/base", params) 215} 216 217func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 218 tpl, err := p.parseRepoBase(name) 219 if err != nil { 220 return err 221 } 222 223 return tpl.ExecuteTemplate(w, "layouts/base", params) 224} 225 226func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 227 tpl, err := p.parseProfileBase(name) 228 if err != nil { 229 return err 230 } 231 232 return tpl.ExecuteTemplate(w, "layouts/base", params) 233} 234 235type DollyParams struct { 236 Classes string 237 FillColor string 238} 239 240func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 241 return p.executePlain("fragments/dolly/logo", w, params) 242} 243 244func (p *Pages) Favicon(w io.Writer) error { 245 return p.Dolly(w, DollyParams{ 246 Classes: "text-black dark:text-white", 247 }) 248} 249 250type LoginParams struct { 251 ReturnUrl string 252 ErrorCode string 253 AddAccount bool 254 LoggedInUser *oauth.MultiAccountUser 255} 256 257func (p *Pages) Login(w io.Writer, params LoginParams) error { 258 return p.executeLogin("user/login", w, params) 259} 260 261type SignupParams struct { 262 CloudflareSiteKey string 263 EmailId string 264} 265 266func (p *Pages) Signup(w io.Writer, params SignupParams) error { 267 return p.executeLogin("user/signup", w, params) 268} 269 270func (p *Pages) CompleteSignup(w io.Writer) error { 271 return p.executeLogin("user/completeSignup", w, nil) 272} 273 274type TermsOfServiceParams struct { 275 LoggedInUser *oauth.MultiAccountUser 276 Content template.HTML 277} 278 279func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 280 filename := "terms.md" 281 filePath := filepath.Join("legal", filename) 282 283 file, err := p.embedFS.Open(filePath) 284 if err != nil { 285 return fmt.Errorf("failed to read %s: %w", filename, err) 286 } 287 defer file.Close() 288 289 markdownBytes, err := io.ReadAll(file) 290 if err != nil { 291 return fmt.Errorf("failed to read %s: %w", filename, err) 292 } 293 294 p.rctx.RendererType = markup.RendererTypeDefault 295 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 296 sanitized := p.rctx.SanitizeDefault(htmlString) 297 params.Content = template.HTML(sanitized) 298 299 return p.execute("legal/terms", w, params) 300} 301 302type PrivacyPolicyParams struct { 303 LoggedInUser *oauth.MultiAccountUser 304 Content template.HTML 305} 306 307func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 308 filename := "privacy.md" 309 filePath := filepath.Join("legal", filename) 310 311 file, err := p.embedFS.Open(filePath) 312 if err != nil { 313 return fmt.Errorf("failed to read %s: %w", filename, err) 314 } 315 defer file.Close() 316 317 markdownBytes, err := io.ReadAll(file) 318 if err != nil { 319 return fmt.Errorf("failed to read %s: %w", filename, err) 320 } 321 322 p.rctx.RendererType = markup.RendererTypeDefault 323 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 324 sanitized := p.rctx.SanitizeDefault(htmlString) 325 params.Content = template.HTML(sanitized) 326 327 return p.execute("legal/privacy", w, params) 328} 329 330type BrandParams struct { 331 LoggedInUser *oauth.MultiAccountUser 332} 333 334func (p *Pages) Brand(w io.Writer, params BrandParams) error { 335 return p.execute("brand/brand", w, params) 336} 337 338type TimelineParams struct { 339 LoggedInUser *oauth.MultiAccountUser 340 Timeline []models.TimelineEvent 341 Repos []models.Repo 342 GfiLabel *models.LabelDefinition 343 BlueskyPosts []models.BskyPost 344} 345 346func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 347 return p.execute("timeline/timeline", w, params) 348} 349 350type GoodFirstIssuesParams struct { 351 LoggedInUser *oauth.MultiAccountUser 352 Issues []models.Issue 353 RepoGroups []*models.RepoGroup 354 LabelDefs map[string]*models.LabelDefinition 355 GfiLabel *models.LabelDefinition 356 Page pagination.Page 357} 358 359func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 360 return p.execute("goodfirstissues/index", w, params) 361} 362 363type UserProfileSettingsParams struct { 364 LoggedInUser *oauth.MultiAccountUser 365 Tab string 366 PunchcardPreference models.PunchcardPreference 367} 368 369func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 370 params.Tab = "profile" 371 return p.execute("user/settings/profile", w, params) 372} 373 374type NotificationsParams struct { 375 LoggedInUser *oauth.MultiAccountUser 376 Notifications []*models.NotificationWithEntity 377 UnreadCount int 378 Page pagination.Page 379 Total int64 380} 381 382func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 383 return p.execute("notifications/list", w, params) 384} 385 386type NotificationItemParams struct { 387 Notification *models.Notification 388} 389 390func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 391 return p.executePlain("notifications/fragments/item", w, params) 392} 393 394type NotificationCountParams struct { 395 Count int64 396} 397 398func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 399 return p.executePlain("notifications/fragments/count", w, params) 400} 401 402type UserKeysSettingsParams struct { 403 LoggedInUser *oauth.MultiAccountUser 404 PubKeys []models.PublicKey 405 Tab string 406} 407 408func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 409 params.Tab = "keys" 410 return p.execute("user/settings/keys", w, params) 411} 412 413type UserEmailsSettingsParams struct { 414 LoggedInUser *oauth.MultiAccountUser 415 Emails []models.Email 416 Tab string 417} 418 419func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 420 params.Tab = "emails" 421 return p.execute("user/settings/emails", w, params) 422} 423 424type UserNotificationSettingsParams struct { 425 LoggedInUser *oauth.MultiAccountUser 426 Preferences *models.NotificationPreferences 427 Tab string 428} 429 430func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 431 params.Tab = "notifications" 432 return p.execute("user/settings/notifications", w, params) 433} 434 435type UserSiteSettingsParams struct { 436 LoggedInUser *oauth.MultiAccountUser 437 Claim *models.DomainClaim 438 SitesDomain string 439 IsTnglHandle bool 440 Tab string 441} 442 443func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 444 params.Tab = "sites" 445 return p.execute("user/settings/sites", w, params) 446} 447 448type UpgradeBannerParams struct { 449 Registrations []models.Registration 450 Spindles []models.Spindle 451} 452 453func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 454 return p.executePlain("banner", w, params) 455} 456 457type KnotsParams struct { 458 LoggedInUser *oauth.MultiAccountUser 459 Registrations []models.Registration 460 Tab string 461} 462 463func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 464 params.Tab = "knots" 465 return p.execute("knots/index", w, params) 466} 467 468type KnotParams struct { 469 LoggedInUser *oauth.MultiAccountUser 470 Registration *models.Registration 471 Members []string 472 Repos map[string][]models.Repo 473 IsOwner bool 474 Tab string 475} 476 477func (p *Pages) Knot(w io.Writer, params KnotParams) error { 478 return p.execute("knots/dashboard", w, params) 479} 480 481type KnotListingParams struct { 482 *models.Registration 483} 484 485func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 486 return p.executePlain("knots/fragments/knotListing", w, params) 487} 488 489type SpindlesParams struct { 490 LoggedInUser *oauth.MultiAccountUser 491 Spindles []models.Spindle 492 Tab string 493} 494 495func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 496 params.Tab = "spindles" 497 return p.execute("spindles/index", w, params) 498} 499 500type SpindleListingParams struct { 501 models.Spindle 502 Tab string 503} 504 505func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 506 return p.executePlain("spindles/fragments/spindleListing", w, params) 507} 508 509type SpindleDashboardParams struct { 510 LoggedInUser *oauth.MultiAccountUser 511 Spindle models.Spindle 512 Members []string 513 Repos map[string][]models.Repo 514 Tab string 515} 516 517func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 518 return p.execute("spindles/dashboard", w, params) 519} 520 521type NewRepoParams struct { 522 LoggedInUser *oauth.MultiAccountUser 523 Knots []string 524} 525 526func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 527 return p.execute("repo/new", w, params) 528} 529 530type ForkRepoParams struct { 531 LoggedInUser *oauth.MultiAccountUser 532 Knots []string 533 RepoInfo repoinfo.RepoInfo 534} 535 536func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 537 return p.execute("repo/fork", w, params) 538} 539 540type ProfileCard struct { 541 UserDid string 542 HasProfile bool 543 FollowStatus models.FollowStatus 544 Punchcard *models.Punchcard 545 Profile *models.Profile 546 Stats ProfileStats 547 Active string 548} 549 550type ProfileStats struct { 551 RepoCount int64 552 StarredCount int64 553 StringCount int64 554 FollowersCount int64 555 FollowingCount int64 556} 557 558func (p *ProfileCard) GetTabs() [][]any { 559 tabs := [][]any{ 560 {"overview", "overview", "square-chart-gantt", nil}, 561 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 562 {"starred", "starred", "star", p.Stats.StarredCount}, 563 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 564 } 565 566 return tabs 567} 568 569type ProfileOverviewParams struct { 570 LoggedInUser *oauth.MultiAccountUser 571 Repos []models.Repo 572 CollaboratingRepos []models.Repo 573 ProfileTimeline *models.ProfileTimeline 574 Card *ProfileCard 575 Active string 576 ShowPunchcard bool 577} 578 579func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 580 params.Active = "overview" 581 return p.executeProfile("user/overview", w, params) 582} 583 584type ProfileReposParams struct { 585 LoggedInUser *oauth.MultiAccountUser 586 Repos []models.Repo 587 Card *ProfileCard 588 Active string 589} 590 591func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 592 params.Active = "repos" 593 return p.executeProfile("user/repos", w, params) 594} 595 596type ProfileStarredParams struct { 597 LoggedInUser *oauth.MultiAccountUser 598 Repos []models.Repo 599 Card *ProfileCard 600 Active string 601} 602 603func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 604 params.Active = "starred" 605 return p.executeProfile("user/starred", w, params) 606} 607 608type ProfileStringsParams struct { 609 LoggedInUser *oauth.MultiAccountUser 610 Strings []models.String 611 Card *ProfileCard 612 Active string 613} 614 615func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 616 params.Active = "strings" 617 return p.executeProfile("user/strings", w, params) 618} 619 620type FollowCard struct { 621 UserDid string 622 LoggedInUser *oauth.MultiAccountUser 623 FollowStatus models.FollowStatus 624 FollowersCount int64 625 FollowingCount int64 626 Profile *models.Profile 627} 628 629type ProfileFollowersParams struct { 630 LoggedInUser *oauth.MultiAccountUser 631 Followers []FollowCard 632 Card *ProfileCard 633 Active string 634} 635 636func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 637 params.Active = "overview" 638 return p.executeProfile("user/followers", w, params) 639} 640 641type ProfileFollowingParams struct { 642 LoggedInUser *oauth.MultiAccountUser 643 Following []FollowCard 644 Card *ProfileCard 645 Active string 646} 647 648func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 649 params.Active = "overview" 650 return p.executeProfile("user/following", w, params) 651} 652 653type FollowFragmentParams struct { 654 UserDid string 655 FollowStatus models.FollowStatus 656 FollowersCount int64 657} 658 659func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 660 return p.executePlain("user/fragments/follow-oob", w, params) 661} 662 663type EditBioParams struct { 664 LoggedInUser *oauth.MultiAccountUser 665 Profile *models.Profile 666} 667 668func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 669 return p.executePlain("user/fragments/editBio", w, params) 670} 671 672type EditPinsParams struct { 673 LoggedInUser *oauth.MultiAccountUser 674 Profile *models.Profile 675 AllRepos []PinnedRepo 676} 677 678type PinnedRepo struct { 679 IsPinned bool 680 models.Repo 681} 682 683func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 684 return p.executePlain("user/fragments/editPins", w, params) 685} 686 687type StarBtnFragmentParams struct { 688 IsStarred bool 689 SubjectAt syntax.ATURI 690 StarCount int 691 HxSwapOob bool 692} 693 694func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 695 params.HxSwapOob = true 696 return p.executePlain("fragments/starBtn", w, params) 697} 698 699type RepoIndexParams struct { 700 LoggedInUser *oauth.MultiAccountUser 701 RepoInfo repoinfo.RepoInfo 702 Active string 703 TagMap map[string][]string 704 CommitsTrunc []types.Commit 705 TagsTrunc []*types.TagReference 706 BranchesTrunc []types.Branch 707 // ForkInfo *types.ForkInfo 708 HTMLReadme template.HTML 709 Raw bool 710 EmailToDid map[string]string 711 VerifiedCommits commitverify.VerifiedCommits 712 Languages []types.RepoLanguageDetails 713 Pipelines map[string]models.Pipeline 714 NeedsKnotUpgrade bool 715 KnotUnreachable bool 716 types.RepoIndexResponse 717} 718 719func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 720 params.Active = "overview" 721 if params.IsEmpty { 722 return p.executeRepo("repo/empty", w, params) 723 } 724 725 if params.NeedsKnotUpgrade { 726 return p.executeRepo("repo/needsUpgrade", w, params) 727 } 728 729 if params.KnotUnreachable { 730 return p.executeRepo("repo/knotUnreachable", w, params) 731 } 732 733 p.rctx.RepoInfo = params.RepoInfo 734 p.rctx.RepoInfo.Ref = params.Ref 735 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 736 737 if params.ReadmeFileName != "" { 738 ext := filepath.Ext(params.ReadmeFileName) 739 switch ext { 740 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 741 params.Raw = false 742 htmlString := p.rctx.RenderMarkdown(params.Readme) 743 sanitized := p.rctx.SanitizeDefault(htmlString) 744 params.HTMLReadme = template.HTML(sanitized) 745 default: 746 params.Raw = true 747 } 748 } 749 750 return p.executeRepo("repo/index", w, params) 751} 752 753type RepoLogParams struct { 754 LoggedInUser *oauth.MultiAccountUser 755 RepoInfo repoinfo.RepoInfo 756 TagMap map[string][]string 757 Active string 758 EmailToDid map[string]string 759 VerifiedCommits commitverify.VerifiedCommits 760 Pipelines map[string]models.Pipeline 761 762 types.RepoLogResponse 763} 764 765func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 766 params.Active = "overview" 767 return p.executeRepo("repo/log", w, params) 768} 769 770type RepoCommitParams struct { 771 LoggedInUser *oauth.MultiAccountUser 772 RepoInfo repoinfo.RepoInfo 773 Active string 774 EmailToDid map[string]string 775 Pipeline *models.Pipeline 776 DiffOpts types.DiffOpts 777 778 // singular because it's always going to be just one 779 VerifiedCommit commitverify.VerifiedCommits 780 781 types.RepoCommitResponse 782} 783 784func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 785 params.Active = "overview" 786 return p.executeRepo("repo/commit", w, params) 787} 788 789type RepoTreeParams struct { 790 LoggedInUser *oauth.MultiAccountUser 791 RepoInfo repoinfo.RepoInfo 792 Active string 793 BreadCrumbs [][]string 794 TreePath string 795 Raw bool 796 HTMLReadme template.HTML 797 EmailToDid map[string]string 798 LastCommitInfo *types.LastCommitInfo 799 types.RepoTreeResponse 800} 801 802type RepoTreeStats struct { 803 NumFolders uint64 804 NumFiles uint64 805} 806 807func (r RepoTreeParams) TreeStats() RepoTreeStats { 808 numFolders, numFiles := 0, 0 809 for _, f := range r.Files { 810 if !f.IsFile() { 811 numFolders += 1 812 } else if f.IsFile() { 813 numFiles += 1 814 } 815 } 816 817 return RepoTreeStats{ 818 NumFolders: uint64(numFolders), 819 NumFiles: uint64(numFiles), 820 } 821} 822 823func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 824 params.Active = "overview" 825 826 p.rctx.RepoInfo = params.RepoInfo 827 p.rctx.RepoInfo.Ref = params.Ref 828 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 829 830 if params.ReadmeFileName != "" { 831 ext := filepath.Ext(params.ReadmeFileName) 832 switch ext { 833 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 834 params.Raw = false 835 htmlString := p.rctx.RenderMarkdown(params.Readme) 836 sanitized := p.rctx.SanitizeDefault(htmlString) 837 params.HTMLReadme = template.HTML(sanitized) 838 default: 839 params.Raw = true 840 } 841 } 842 843 return p.executeRepo("repo/tree", w, params) 844} 845 846type RepoBranchesParams struct { 847 LoggedInUser *oauth.MultiAccountUser 848 RepoInfo repoinfo.RepoInfo 849 Active string 850 types.RepoBranchesResponse 851} 852 853func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 854 params.Active = "overview" 855 return p.executeRepo("repo/branches", w, params) 856} 857 858type RepoTagsParams struct { 859 LoggedInUser *oauth.MultiAccountUser 860 RepoInfo repoinfo.RepoInfo 861 Active string 862 types.RepoTagsResponse 863 ArtifactMap map[plumbing.Hash][]models.Artifact 864 DanglingArtifacts []models.Artifact 865} 866 867func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 868 params.Active = "overview" 869 return p.executeRepo("repo/tags", w, params) 870} 871 872type RepoTagParams struct { 873 LoggedInUser *oauth.MultiAccountUser 874 RepoInfo repoinfo.RepoInfo 875 Active string 876 types.RepoTagResponse 877 ArtifactMap map[plumbing.Hash][]models.Artifact 878 DanglingArtifacts []models.Artifact 879} 880 881func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 882 params.Active = "overview" 883 return p.executeRepo("repo/tag", w, params) 884} 885 886type RepoArtifactParams struct { 887 LoggedInUser *oauth.MultiAccountUser 888 RepoInfo repoinfo.RepoInfo 889 Artifact models.Artifact 890} 891 892func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 893 return p.executePlain("repo/fragments/artifact", w, params) 894} 895 896type RepoBlobParams struct { 897 LoggedInUser *oauth.MultiAccountUser 898 RepoInfo repoinfo.RepoInfo 899 Active string 900 BreadCrumbs [][]string 901 BlobView models.BlobView 902 EmailToDid map[string]string 903 LastCommitInfo *types.LastCommitInfo 904 *tangled.RepoBlob_Output 905} 906 907func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 908 switch params.BlobView.ContentType { 909 case models.BlobContentTypeMarkup: 910 p.rctx.RepoInfo = params.RepoInfo 911 } 912 913 params.Active = "overview" 914 return p.executeRepo("repo/blob", w, params) 915} 916 917type Collaborator struct { 918 Did string 919 Role string 920} 921 922type RepoSettingsParams struct { 923 LoggedInUser *oauth.MultiAccountUser 924 RepoInfo repoinfo.RepoInfo 925 Collaborators []Collaborator 926 Active string 927 Branches []types.Branch 928 Spindles []string 929 CurrentSpindle string 930 Secrets []*tangled.RepoListSecrets_Secret 931 932 // TODO: use repoinfo.roles 933 IsCollaboratorInviteAllowed bool 934} 935 936func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 937 params.Active = "settings" 938 return p.executeRepo("repo/settings", w, params) 939} 940 941type RepoGeneralSettingsParams struct { 942 LoggedInUser *oauth.MultiAccountUser 943 RepoInfo repoinfo.RepoInfo 944 Labels []models.LabelDefinition 945 DefaultLabels []models.LabelDefinition 946 SubscribedLabels map[string]struct{} 947 ShouldSubscribeAll bool 948 Active string 949 Tab string 950 Branches []types.Branch 951} 952 953func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 954 params.Active = "settings" 955 params.Tab = "general" 956 return p.executeRepo("repo/settings/general", w, params) 957} 958 959type RepoAccessSettingsParams struct { 960 LoggedInUser *oauth.MultiAccountUser 961 RepoInfo repoinfo.RepoInfo 962 Active string 963 Tab string 964 Collaborators []Collaborator 965} 966 967func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 968 params.Active = "settings" 969 params.Tab = "access" 970 return p.executeRepo("repo/settings/access", w, params) 971} 972 973type RepoPipelineSettingsParams struct { 974 LoggedInUser *oauth.MultiAccountUser 975 RepoInfo repoinfo.RepoInfo 976 Active string 977 Tab string 978 Spindles []string 979 CurrentSpindle string 980 Secrets []map[string]any 981} 982 983func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 984 params.Active = "settings" 985 params.Tab = "pipelines" 986 return p.executeRepo("repo/settings/pipelines", w, params) 987} 988 989type RepoWebhooksSettingsParams struct { 990 LoggedInUser *oauth.MultiAccountUser 991 RepoInfo repoinfo.RepoInfo 992 Active string 993 Tab string 994 Webhooks []models.Webhook 995 WebhookDeliveries map[int64][]models.WebhookDelivery 996} 997 998func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 999 params.Active = "settings" 1000 params.Tab = "hooks" 1001 return p.executeRepo("repo/settings/hooks", w, params) 1002} 1003 1004type WebhookDeliveriesListParams struct { 1005 LoggedInUser *oauth.MultiAccountUser 1006 RepoInfo repoinfo.RepoInfo 1007 Webhook *models.Webhook 1008 Deliveries []models.WebhookDelivery 1009} 1010 1011func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1012 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1013 if err != nil { 1014 return err 1015 } 1016 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1017} 1018 1019type RepoSiteSettingsParams struct { 1020 LoggedInUser *oauth.MultiAccountUser 1021 RepoInfo repoinfo.RepoInfo 1022 Active string 1023 Tab string 1024 Branches []types.Branch 1025 SiteConfig *models.RepoSite 1026 OwnerClaim *models.DomainClaim 1027 Deploys []models.SiteDeploy 1028 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1029} 1030 1031func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1032 params.Active = "settings" 1033 params.Tab = "sites" 1034 return p.executeRepo("repo/settings/sites", w, params) 1035} 1036 1037type RepoIssuesParams struct { 1038 LoggedInUser *oauth.MultiAccountUser 1039 RepoInfo repoinfo.RepoInfo 1040 Active string 1041 Issues []models.Issue 1042 IssueCount int 1043 LabelDefs map[string]*models.LabelDefinition 1044 Page pagination.Page 1045 FilterState string 1046 FilterQuery string 1047} 1048 1049func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1050 params.Active = "issues" 1051 return p.executeRepo("repo/issues/issues", w, params) 1052} 1053 1054type RepoSingleIssueParams struct { 1055 LoggedInUser *oauth.MultiAccountUser 1056 RepoInfo repoinfo.RepoInfo 1057 Active string 1058 Issue *models.Issue 1059 CommentList []models.CommentListItem 1060 Backlinks []models.RichReferenceLink 1061 LabelDefs map[string]*models.LabelDefinition 1062 1063 Reactions map[models.ReactionKind]models.ReactionDisplayData 1064 UserReacted map[models.ReactionKind]bool 1065} 1066 1067func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1068 params.Active = "issues" 1069 return p.executeRepo("repo/issues/issue", w, params) 1070} 1071 1072type EditIssueParams struct { 1073 LoggedInUser *oauth.MultiAccountUser 1074 RepoInfo repoinfo.RepoInfo 1075 Issue *models.Issue 1076 Action string 1077} 1078 1079func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1080 params.Action = "edit" 1081 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1082} 1083 1084type ThreadReactionFragmentParams struct { 1085 ThreadAt syntax.ATURI 1086 Kind models.ReactionKind 1087 Count int 1088 Users []string 1089 IsReacted bool 1090} 1091 1092func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1093 return p.executePlain("repo/fragments/reaction", w, params) 1094} 1095 1096type RepoNewIssueParams struct { 1097 LoggedInUser *oauth.MultiAccountUser 1098 RepoInfo repoinfo.RepoInfo 1099 Issue *models.Issue // existing issue if any -- passed when editing 1100 Active string 1101 Action string 1102} 1103 1104func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1105 params.Active = "issues" 1106 params.Action = "create" 1107 return p.executeRepo("repo/issues/new", w, params) 1108} 1109 1110type EditIssueCommentParams struct { 1111 LoggedInUser *oauth.MultiAccountUser 1112 RepoInfo repoinfo.RepoInfo 1113 Issue *models.Issue 1114 Comment *models.IssueComment 1115} 1116 1117func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1118 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1119} 1120 1121type ReplyIssueCommentPlaceholderParams struct { 1122 LoggedInUser *oauth.MultiAccountUser 1123 RepoInfo repoinfo.RepoInfo 1124 Issue *models.Issue 1125 Comment *models.IssueComment 1126} 1127 1128func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1129 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1130} 1131 1132type ReplyIssueCommentParams struct { 1133 LoggedInUser *oauth.MultiAccountUser 1134 RepoInfo repoinfo.RepoInfo 1135 Issue *models.Issue 1136 Comment *models.IssueComment 1137} 1138 1139func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1140 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1141} 1142 1143type IssueCommentBodyParams struct { 1144 LoggedInUser *oauth.MultiAccountUser 1145 RepoInfo repoinfo.RepoInfo 1146 Issue *models.Issue 1147 Comment *models.IssueComment 1148} 1149 1150func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1151 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1152} 1153 1154type RepoNewPullParams struct { 1155 LoggedInUser *oauth.MultiAccountUser 1156 RepoInfo repoinfo.RepoInfo 1157 Branches []types.Branch 1158 Strategy string 1159 SourceBranch string 1160 TargetBranch string 1161 Title string 1162 Body string 1163 Active string 1164} 1165 1166func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1167 params.Active = "pulls" 1168 return p.executeRepo("repo/pulls/new", w, params) 1169} 1170 1171type RepoPullsParams struct { 1172 LoggedInUser *oauth.MultiAccountUser 1173 RepoInfo repoinfo.RepoInfo 1174 Pulls []*models.Pull 1175 Active string 1176 FilterState string 1177 FilterQuery string 1178 Stacks map[string]models.Stack 1179 Pipelines map[string]models.Pipeline 1180 LabelDefs map[string]*models.LabelDefinition 1181 Page pagination.Page 1182 PullCount int 1183} 1184 1185func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1186 params.Active = "pulls" 1187 return p.executeRepo("repo/pulls/pulls", w, params) 1188} 1189 1190type ResubmitResult uint64 1191 1192const ( 1193 ShouldResubmit ResubmitResult = iota 1194 ShouldNotResubmit 1195 Unknown 1196) 1197 1198func (r ResubmitResult) Yes() bool { 1199 return r == ShouldResubmit 1200} 1201func (r ResubmitResult) No() bool { 1202 return r == ShouldNotResubmit 1203} 1204func (r ResubmitResult) Unknown() bool { 1205 return r == Unknown 1206} 1207 1208type RepoSinglePullParams struct { 1209 LoggedInUser *oauth.MultiAccountUser 1210 RepoInfo repoinfo.RepoInfo 1211 Active string 1212 Pull *models.Pull 1213 Stack models.Stack 1214 AbandonedPulls []*models.Pull 1215 Backlinks []models.RichReferenceLink 1216 BranchDeleteStatus *models.BranchDeleteStatus 1217 MergeCheck types.MergeCheckResponse 1218 ResubmitCheck ResubmitResult 1219 Pipelines map[string]models.Pipeline 1220 Diff types.DiffRenderer 1221 DiffOpts types.DiffOpts 1222 ActiveRound int 1223 IsInterdiff bool 1224 1225 Reactions map[models.ReactionKind]models.ReactionDisplayData 1226 UserReacted map[models.ReactionKind]bool 1227 1228 LabelDefs map[string]*models.LabelDefinition 1229} 1230 1231func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1232 params.Active = "pulls" 1233 return p.executeRepo("repo/pulls/pull", w, params) 1234} 1235 1236type RepoPullPatchParams struct { 1237 LoggedInUser *oauth.MultiAccountUser 1238 RepoInfo repoinfo.RepoInfo 1239 Pull *models.Pull 1240 Stack models.Stack 1241 Diff *types.NiceDiff 1242 Round int 1243 Submission *models.PullSubmission 1244 DiffOpts types.DiffOpts 1245} 1246 1247// this name is a mouthful 1248func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1249 return p.execute("repo/pulls/patch", w, params) 1250} 1251 1252type RepoPullInterdiffParams struct { 1253 LoggedInUser *oauth.MultiAccountUser 1254 RepoInfo repoinfo.RepoInfo 1255 Pull *models.Pull 1256 Round int 1257 Interdiff *patchutil.InterdiffResult 1258 DiffOpts types.DiffOpts 1259} 1260 1261// this name is a mouthful 1262func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1263 return p.execute("repo/pulls/interdiff", w, params) 1264} 1265 1266type PullPatchUploadParams struct { 1267 RepoInfo repoinfo.RepoInfo 1268} 1269 1270func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1271 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1272} 1273 1274type PullCompareBranchesParams struct { 1275 RepoInfo repoinfo.RepoInfo 1276 Branches []types.Branch 1277 SourceBranch string 1278} 1279 1280func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1281 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1282} 1283 1284type PullCompareForkParams struct { 1285 RepoInfo repoinfo.RepoInfo 1286 Forks []models.Repo 1287 Selected string 1288} 1289 1290func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1291 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1292} 1293 1294type PullCompareForkBranchesParams struct { 1295 RepoInfo repoinfo.RepoInfo 1296 SourceBranches []types.Branch 1297 TargetBranches []types.Branch 1298} 1299 1300func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1301 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1302} 1303 1304type PullResubmitParams struct { 1305 LoggedInUser *oauth.MultiAccountUser 1306 RepoInfo repoinfo.RepoInfo 1307 Pull *models.Pull 1308 SubmissionId int 1309} 1310 1311func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1312 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1313} 1314 1315type PullActionsParams struct { 1316 LoggedInUser *oauth.MultiAccountUser 1317 RepoInfo repoinfo.RepoInfo 1318 Pull *models.Pull 1319 RoundNumber int 1320 MergeCheck types.MergeCheckResponse 1321 ResubmitCheck ResubmitResult 1322 BranchDeleteStatus *models.BranchDeleteStatus 1323 Stack models.Stack 1324} 1325 1326func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1327 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1328} 1329 1330type PullNewCommentParams struct { 1331 LoggedInUser *oauth.MultiAccountUser 1332 RepoInfo repoinfo.RepoInfo 1333 Pull *models.Pull 1334 RoundNumber int 1335} 1336 1337func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1338 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1339} 1340 1341type RepoCompareParams struct { 1342 LoggedInUser *oauth.MultiAccountUser 1343 RepoInfo repoinfo.RepoInfo 1344 Forks []models.Repo 1345 Branches []types.Branch 1346 Tags []*types.TagReference 1347 Base string 1348 Head string 1349 Diff *types.NiceDiff 1350 DiffOpts types.DiffOpts 1351 1352 Active string 1353} 1354 1355func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1356 params.Active = "overview" 1357 return p.executeRepo("repo/compare/compare", w, params) 1358} 1359 1360type RepoCompareNewParams struct { 1361 LoggedInUser *oauth.MultiAccountUser 1362 RepoInfo repoinfo.RepoInfo 1363 Forks []models.Repo 1364 Branches []types.Branch 1365 Tags []*types.TagReference 1366 Base string 1367 Head string 1368 1369 Active string 1370} 1371 1372func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1373 params.Active = "overview" 1374 return p.executeRepo("repo/compare/new", w, params) 1375} 1376 1377type RepoCompareAllowPullParams struct { 1378 LoggedInUser *oauth.MultiAccountUser 1379 RepoInfo repoinfo.RepoInfo 1380 Base string 1381 Head string 1382} 1383 1384func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1385 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1386} 1387 1388type RepoCompareDiffFragmentParams struct { 1389 Diff types.NiceDiff 1390 DiffOpts types.DiffOpts 1391} 1392 1393func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1394 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1395} 1396 1397type LabelPanelParams struct { 1398 LoggedInUser *oauth.MultiAccountUser 1399 RepoInfo repoinfo.RepoInfo 1400 Defs map[string]*models.LabelDefinition 1401 Subject string 1402 State models.LabelState 1403} 1404 1405func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1406 return p.executePlain("repo/fragments/labelPanel", w, params) 1407} 1408 1409type EditLabelPanelParams struct { 1410 LoggedInUser *oauth.MultiAccountUser 1411 RepoInfo repoinfo.RepoInfo 1412 Defs map[string]*models.LabelDefinition 1413 Subject string 1414 State models.LabelState 1415} 1416 1417func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1418 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1419} 1420 1421type PipelinesParams struct { 1422 LoggedInUser *oauth.MultiAccountUser 1423 RepoInfo repoinfo.RepoInfo 1424 Pipelines []models.Pipeline 1425 Active string 1426 FilterKind string 1427 Total int64 1428} 1429 1430func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1431 params.Active = "pipelines" 1432 return p.executeRepo("repo/pipelines/pipelines", w, params) 1433} 1434 1435type LogBlockParams struct { 1436 Id int 1437 Name string 1438 Command string 1439 Collapsed bool 1440 StartTime time.Time 1441} 1442 1443func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1444 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1445} 1446 1447type LogBlockEndParams struct { 1448 Id int 1449 StartTime time.Time 1450 EndTime time.Time 1451} 1452 1453func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1454 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1455} 1456 1457type LogLineParams struct { 1458 Id int 1459 Content string 1460} 1461 1462func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1463 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1464} 1465 1466type WorkflowParams struct { 1467 LoggedInUser *oauth.MultiAccountUser 1468 RepoInfo repoinfo.RepoInfo 1469 Pipeline models.Pipeline 1470 Workflow string 1471 LogUrl string 1472 Active string 1473} 1474 1475func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1476 params.Active = "pipelines" 1477 return p.executeRepo("repo/pipelines/workflow", w, params) 1478} 1479 1480type PutStringParams struct { 1481 LoggedInUser *oauth.MultiAccountUser 1482 Action string 1483 1484 // this is supplied in the case of editing an existing string 1485 String models.String 1486} 1487 1488func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1489 return p.execute("strings/put", w, params) 1490} 1491 1492type StringsDashboardParams struct { 1493 LoggedInUser *oauth.MultiAccountUser 1494 Card ProfileCard 1495 Strings []models.String 1496} 1497 1498func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1499 return p.execute("strings/dashboard", w, params) 1500} 1501 1502type StringTimelineParams struct { 1503 LoggedInUser *oauth.MultiAccountUser 1504 Strings []models.String 1505} 1506 1507func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1508 return p.execute("strings/timeline", w, params) 1509} 1510 1511type SingleStringParams struct { 1512 LoggedInUser *oauth.MultiAccountUser 1513 ShowRendered bool 1514 RenderToggle bool 1515 RenderedContents template.HTML 1516 String *models.String 1517 Stats models.StringStats 1518 IsStarred bool 1519 StarCount int 1520 Owner identity.Identity 1521} 1522 1523func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1524 return p.execute("strings/string", w, params) 1525} 1526 1527func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1528 return p.execute("timeline/home", w, params) 1529} 1530 1531func (p *Pages) Static() http.Handler { 1532 if p.dev { 1533 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1534 } 1535 1536 sub, err := fs.Sub(p.embedFS, "static") 1537 if err != nil { 1538 p.logger.Error("no static dir found? that's crazy", "err", err) 1539 panic(err) 1540 } 1541 // Custom handler to apply Cache-Control headers for font files 1542 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1543} 1544 1545func Cache(h http.Handler) http.Handler { 1546 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1547 path := strings.Split(r.URL.Path, "?")[0] 1548 1549 if strings.HasSuffix(path, ".css") { 1550 // on day for css files 1551 w.Header().Set("Cache-Control", "public, max-age=86400") 1552 } else { 1553 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1554 } 1555 h.ServeHTTP(w, r) 1556 }) 1557} 1558 1559func (p *Pages) CssContentHash() string { 1560 cssFile, err := p.embedFS.Open("static/tw.css") 1561 if err != nil { 1562 slog.Debug("Error opening CSS file", "err", err) 1563 return "" 1564 } 1565 defer cssFile.Close() 1566 1567 hasher := sha256.New() 1568 if _, err := io.Copy(hasher, cssFile); err != nil { 1569 slog.Debug("Error hashing CSS file", "err", err) 1570 return "" 1571 } 1572 1573 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1574} 1575 1576func (p *Pages) Error500(w io.Writer) error { 1577 return p.execute("errors/500", w, nil) 1578} 1579 1580func (p *Pages) Error404(w io.Writer) error { 1581 return p.execute("errors/404", w, nil) 1582} 1583 1584func (p *Pages) ErrorKnot404(w io.Writer) error { 1585 return p.execute("errors/knot404", w, nil) 1586} 1587 1588func (p *Pages) Error503(w io.Writer) error { 1589 return p.execute("errors/503", w, nil) 1590}