this repo has no description
1package pages 2 3import ( 4 "bytes" 5 "crypto/sha256" 6 "embed" 7 "encoding/hex" 8 "fmt" 9 "html/template" 10 "io" 11 "io/fs" 12 "log" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 "sync" 18 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview/commitverify" 21 "tangled.sh/tangled.sh/core/appview/config" 22 "tangled.sh/tangled.sh/core/appview/db" 23 "tangled.sh/tangled.sh/core/appview/oauth" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" 25 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 "tangled.sh/tangled.sh/core/appview/pagination" 27 "tangled.sh/tangled.sh/core/patchutil" 28 "tangled.sh/tangled.sh/core/types" 29 30 "github.com/alecthomas/chroma/v2" 31 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 32 "github.com/alecthomas/chroma/v2/lexers" 33 "github.com/alecthomas/chroma/v2/styles" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 "github.com/go-git/go-git/v5/plumbing" 36 "github.com/go-git/go-git/v5/plumbing/object" 37) 38 39//go:embed templates/* static 40var Files embed.FS 41 42type Pages struct { 43 mu sync.RWMutex 44 t map[string]*template.Template 45 46 avatar config.AvatarConfig 47 dev bool 48 embedFS embed.FS 49 templateDir string // Path to templates on disk for dev mode 50 rctx *markup.RenderContext 51} 52 53func NewPages(config *config.Config) *Pages { 54 // initialized with safe defaults, can be overriden per use 55 rctx := &markup.RenderContext{ 56 IsDev: config.Core.Dev, 57 CamoUrl: config.Camo.Host, 58 CamoSecret: config.Camo.SharedSecret, 59 } 60 61 p := &Pages{ 62 mu: sync.RWMutex{}, 63 t: make(map[string]*template.Template), 64 dev: config.Core.Dev, 65 avatar: config.Avatar, 66 embedFS: Files, 67 rctx: rctx, 68 templateDir: "appview/pages", 69 } 70 71 // Initial load of all templates 72 p.loadAllTemplates() 73 74 return p 75} 76 77func (p *Pages) loadAllTemplates() { 78 templates := make(map[string]*template.Template) 79 var fragmentPaths []string 80 81 // Use embedded FS for initial loading 82 // First, collect all fragment paths 83 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 84 if err != nil { 85 return err 86 } 87 if d.IsDir() { 88 return nil 89 } 90 if !strings.HasSuffix(path, ".html") { 91 return nil 92 } 93 if !strings.Contains(path, "fragments/") { 94 return nil 95 } 96 name := strings.TrimPrefix(path, "templates/") 97 name = strings.TrimSuffix(name, ".html") 98 tmpl, err := template.New(name). 99 Funcs(p.funcMap()). 100 ParseFS(p.embedFS, path) 101 if err != nil { 102 log.Fatalf("setting up fragment: %v", err) 103 } 104 templates[name] = tmpl 105 fragmentPaths = append(fragmentPaths, path) 106 log.Printf("loaded fragment: %s", name) 107 return nil 108 }) 109 if err != nil { 110 log.Fatalf("walking template dir for fragments: %v", err) 111 } 112 113 // Then walk through and setup the rest of the templates 114 err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 115 if err != nil { 116 return err 117 } 118 if d.IsDir() { 119 return nil 120 } 121 if !strings.HasSuffix(path, "html") { 122 return nil 123 } 124 // Skip fragments as they've already been loaded 125 if strings.Contains(path, "fragments/") { 126 return nil 127 } 128 // Skip layouts 129 if strings.Contains(path, "layouts/") { 130 return nil 131 } 132 name := strings.TrimPrefix(path, "templates/") 133 name = strings.TrimSuffix(name, ".html") 134 // Add the page template on top of the base 135 allPaths := []string{} 136 allPaths = append(allPaths, "templates/layouts/*.html") 137 allPaths = append(allPaths, fragmentPaths...) 138 allPaths = append(allPaths, path) 139 tmpl, err := template.New(name). 140 Funcs(p.funcMap()). 141 ParseFS(p.embedFS, allPaths...) 142 if err != nil { 143 return fmt.Errorf("setting up template: %w", err) 144 } 145 templates[name] = tmpl 146 log.Printf("loaded template: %s", name) 147 return nil 148 }) 149 if err != nil { 150 log.Fatalf("walking template dir: %v", err) 151 } 152 153 log.Printf("total templates loaded: %d", len(templates)) 154 p.mu.Lock() 155 defer p.mu.Unlock() 156 p.t = templates 157} 158 159// loadTemplateFromDisk loads a template from the filesystem in dev mode 160func (p *Pages) loadTemplateFromDisk(name string) error { 161 if !p.dev { 162 return nil 163 } 164 165 log.Printf("reloading template from disk: %s", name) 166 167 // Find all fragments first 168 var fragmentPaths []string 169 err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 170 if err != nil { 171 return err 172 } 173 if d.IsDir() { 174 return nil 175 } 176 if !strings.HasSuffix(path, ".html") { 177 return nil 178 } 179 if !strings.Contains(path, "fragments/") { 180 return nil 181 } 182 fragmentPaths = append(fragmentPaths, path) 183 return nil 184 }) 185 if err != nil { 186 return fmt.Errorf("walking disk template dir for fragments: %w", err) 187 } 188 189 // Find the template path on disk 190 templatePath := filepath.Join(p.templateDir, "templates", name+".html") 191 if _, err := os.Stat(templatePath); os.IsNotExist(err) { 192 return fmt.Errorf("template not found on disk: %s", name) 193 } 194 195 // Create a new template 196 tmpl := template.New(name).Funcs(p.funcMap()) 197 198 // Parse layouts 199 layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 200 layouts, err := filepath.Glob(layoutGlob) 201 if err != nil { 202 return fmt.Errorf("finding layout templates: %w", err) 203 } 204 205 // Create paths for parsing 206 allFiles := append(layouts, fragmentPaths...) 207 allFiles = append(allFiles, templatePath) 208 209 // Parse all templates 210 tmpl, err = tmpl.ParseFiles(allFiles...) 211 if err != nil { 212 return fmt.Errorf("parsing template files: %w", err) 213 } 214 215 // Update the template in the map 216 p.mu.Lock() 217 defer p.mu.Unlock() 218 p.t[name] = tmpl 219 log.Printf("template reloaded from disk: %s", name) 220 return nil 221} 222 223func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 224 // In dev mode, reload the template from disk before executing 225 if p.dev { 226 if err := p.loadTemplateFromDisk(templateName); err != nil { 227 log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 228 // Continue with the existing template 229 } 230 } 231 232 p.mu.RLock() 233 defer p.mu.RUnlock() 234 tmpl, exists := p.t[templateName] 235 if !exists { 236 return fmt.Errorf("template not found: %s", templateName) 237 } 238 239 if base == "" { 240 return tmpl.Execute(w, params) 241 } else { 242 return tmpl.ExecuteTemplate(w, base, params) 243 } 244} 245 246func (p *Pages) execute(name string, w io.Writer, params any) error { 247 return p.executeOrReload(name, w, "layouts/base", params) 248} 249 250func (p *Pages) executePlain(name string, w io.Writer, params any) error { 251 return p.executeOrReload(name, w, "", params) 252} 253 254func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 255 return p.executeOrReload(name, w, "layouts/repobase", params) 256} 257 258type LoginParams struct { 259} 260 261func (p *Pages) Login(w io.Writer, params LoginParams) error { 262 return p.executePlain("user/login", w, params) 263} 264 265func (p *Pages) Signup(w io.Writer) error { 266 return p.executePlain("user/signup", w, nil) 267} 268 269func (p *Pages) CompleteSignup(w io.Writer) error { 270 return p.executePlain("user/completeSignup", w, nil) 271} 272 273type TermsOfServiceParams struct { 274 LoggedInUser *oauth.User 275} 276 277func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 278 return p.execute("legal/terms", w, params) 279} 280 281type PrivacyPolicyParams struct { 282 LoggedInUser *oauth.User 283} 284 285func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 286 return p.execute("legal/privacy", w, params) 287} 288 289type TimelineParams struct { 290 LoggedInUser *oauth.User 291 Timeline []db.TimelineEvent 292 DidHandleMap map[string]string 293} 294 295func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 296 return p.execute("timeline", w, params) 297} 298 299type SettingsParams struct { 300 LoggedInUser *oauth.User 301 PubKeys []db.PublicKey 302 Emails []db.Email 303} 304 305func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 306 return p.execute("settings", w, params) 307} 308 309type KnotsParams struct { 310 LoggedInUser *oauth.User 311 Registrations []db.Registration 312} 313 314func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 315 return p.execute("knots/index", w, params) 316} 317 318type KnotParams struct { 319 LoggedInUser *oauth.User 320 DidHandleMap map[string]string 321 Registration *db.Registration 322 Members []string 323 Repos map[string][]db.Repo 324 IsOwner bool 325} 326 327func (p *Pages) Knot(w io.Writer, params KnotParams) error { 328 return p.execute("knots/dashboard", w, params) 329} 330 331type KnotListingParams struct { 332 db.Registration 333} 334 335func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 336 return p.executePlain("knots/fragments/knotListing", w, params) 337} 338 339type KnotListingFullParams struct { 340 Registrations []db.Registration 341} 342 343func (p *Pages) KnotListingFull(w io.Writer, params KnotListingFullParams) error { 344 return p.executePlain("knots/fragments/knotListingFull", w, params) 345} 346 347type KnotSecretParams struct { 348 Secret string 349} 350 351func (p *Pages) KnotSecret(w io.Writer, params KnotSecretParams) error { 352 return p.executePlain("knots/fragments/secret", w, params) 353} 354 355type SpindlesParams struct { 356 LoggedInUser *oauth.User 357 Spindles []db.Spindle 358} 359 360func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 361 return p.execute("spindles/index", w, params) 362} 363 364type SpindleListingParams struct { 365 db.Spindle 366} 367 368func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 369 return p.executePlain("spindles/fragments/spindleListing", w, params) 370} 371 372type SpindleDashboardParams struct { 373 LoggedInUser *oauth.User 374 Spindle db.Spindle 375 Members []string 376 Repos map[string][]db.Repo 377 DidHandleMap map[string]string 378} 379 380func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 381 return p.execute("spindles/dashboard", w, params) 382} 383 384type NewRepoParams struct { 385 LoggedInUser *oauth.User 386 Knots []string 387} 388 389func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 390 return p.execute("repo/new", w, params) 391} 392 393type ForkRepoParams struct { 394 LoggedInUser *oauth.User 395 Knots []string 396 RepoInfo repoinfo.RepoInfo 397} 398 399func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 400 return p.execute("repo/fork", w, params) 401} 402 403type ProfilePageParams struct { 404 LoggedInUser *oauth.User 405 Repos []db.Repo 406 CollaboratingRepos []db.Repo 407 ProfileTimeline *db.ProfileTimeline 408 Card ProfileCard 409 Punchcard db.Punchcard 410 411 DidHandleMap map[string]string 412} 413 414type ProfileCard struct { 415 UserDid string 416 UserHandle string 417 FollowStatus db.FollowStatus 418 AvatarUri string 419 Followers int 420 Following int 421 422 Profile *db.Profile 423} 424 425func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 426 return p.execute("user/profile", w, params) 427} 428 429type ReposPageParams struct { 430 LoggedInUser *oauth.User 431 Repos []db.Repo 432 Card ProfileCard 433 434 DidHandleMap map[string]string 435} 436 437func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 438 return p.execute("user/repos", w, params) 439} 440 441type FollowFragmentParams struct { 442 UserDid string 443 FollowStatus db.FollowStatus 444} 445 446func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 447 return p.executePlain("user/fragments/follow", w, params) 448} 449 450type EditBioParams struct { 451 LoggedInUser *oauth.User 452 Profile *db.Profile 453} 454 455func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 456 return p.executePlain("user/fragments/editBio", w, params) 457} 458 459type EditPinsParams struct { 460 LoggedInUser *oauth.User 461 Profile *db.Profile 462 AllRepos []PinnedRepo 463 DidHandleMap map[string]string 464} 465 466type PinnedRepo struct { 467 IsPinned bool 468 db.Repo 469} 470 471func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 472 return p.executePlain("user/fragments/editPins", w, params) 473} 474 475type RepoStarFragmentParams struct { 476 IsStarred bool 477 RepoAt syntax.ATURI 478 Stats db.RepoStats 479} 480 481func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error { 482 return p.executePlain("repo/fragments/repoStar", w, params) 483} 484 485type RepoDescriptionParams struct { 486 RepoInfo repoinfo.RepoInfo 487} 488 489func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 490 return p.executePlain("repo/fragments/editRepoDescription", w, params) 491} 492 493func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 494 return p.executePlain("repo/fragments/repoDescription", w, params) 495} 496 497type RepoIndexParams struct { 498 LoggedInUser *oauth.User 499 RepoInfo repoinfo.RepoInfo 500 Active string 501 TagMap map[string][]string 502 CommitsTrunc []*object.Commit 503 TagsTrunc []*types.TagReference 504 BranchesTrunc []types.Branch 505 ForkInfo *types.ForkInfo 506 HTMLReadme template.HTML 507 Raw bool 508 EmailToDidOrHandle map[string]string 509 VerifiedCommits commitverify.VerifiedCommits 510 Languages []types.RepoLanguageDetails 511 Pipelines map[string]db.Pipeline 512 types.RepoIndexResponse 513} 514 515func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 516 params.Active = "overview" 517 if params.IsEmpty { 518 return p.executeRepo("repo/empty", w, params) 519 } 520 521 p.rctx.RepoInfo = params.RepoInfo 522 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 523 524 if params.ReadmeFileName != "" { 525 var htmlString string 526 ext := filepath.Ext(params.ReadmeFileName) 527 switch ext { 528 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 529 htmlString = p.rctx.Sanitize(htmlString) 530 htmlString = p.rctx.RenderMarkdown(params.Readme) 531 params.Raw = false 532 params.HTMLReadme = template.HTML(htmlString) 533 default: 534 params.Raw = true 535 } 536 } 537 538 return p.executeRepo("repo/index", w, params) 539} 540 541type RepoLogParams struct { 542 LoggedInUser *oauth.User 543 RepoInfo repoinfo.RepoInfo 544 TagMap map[string][]string 545 types.RepoLogResponse 546 Active string 547 EmailToDidOrHandle map[string]string 548 VerifiedCommits commitverify.VerifiedCommits 549 Pipelines map[string]db.Pipeline 550} 551 552func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 553 params.Active = "overview" 554 return p.executeRepo("repo/log", w, params) 555} 556 557type RepoCommitParams struct { 558 LoggedInUser *oauth.User 559 RepoInfo repoinfo.RepoInfo 560 Active string 561 EmailToDidOrHandle map[string]string 562 Pipeline *db.Pipeline 563 DiffOpts types.DiffOpts 564 565 // singular because it's always going to be just one 566 VerifiedCommit commitverify.VerifiedCommits 567 568 types.RepoCommitResponse 569} 570 571func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 572 params.Active = "overview" 573 return p.executeRepo("repo/commit", w, params) 574} 575 576type RepoTreeParams struct { 577 LoggedInUser *oauth.User 578 RepoInfo repoinfo.RepoInfo 579 Active string 580 BreadCrumbs [][]string 581 TreePath string 582 types.RepoTreeResponse 583} 584 585type RepoTreeStats struct { 586 NumFolders uint64 587 NumFiles uint64 588} 589 590func (r RepoTreeParams) TreeStats() RepoTreeStats { 591 numFolders, numFiles := 0, 0 592 for _, f := range r.Files { 593 if !f.IsFile { 594 numFolders += 1 595 } else if f.IsFile { 596 numFiles += 1 597 } 598 } 599 600 return RepoTreeStats{ 601 NumFolders: uint64(numFolders), 602 NumFiles: uint64(numFiles), 603 } 604} 605 606func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 607 params.Active = "overview" 608 return p.execute("repo/tree", w, params) 609} 610 611type RepoBranchesParams struct { 612 LoggedInUser *oauth.User 613 RepoInfo repoinfo.RepoInfo 614 Active string 615 types.RepoBranchesResponse 616} 617 618func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 619 params.Active = "overview" 620 return p.executeRepo("repo/branches", w, params) 621} 622 623type RepoTagsParams struct { 624 LoggedInUser *oauth.User 625 RepoInfo repoinfo.RepoInfo 626 Active string 627 types.RepoTagsResponse 628 ArtifactMap map[plumbing.Hash][]db.Artifact 629 DanglingArtifacts []db.Artifact 630} 631 632func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 633 params.Active = "overview" 634 return p.executeRepo("repo/tags", w, params) 635} 636 637type RepoArtifactParams struct { 638 LoggedInUser *oauth.User 639 RepoInfo repoinfo.RepoInfo 640 Artifact db.Artifact 641} 642 643func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 644 return p.executePlain("repo/fragments/artifact", w, params) 645} 646 647type RepoBlobParams struct { 648 LoggedInUser *oauth.User 649 RepoInfo repoinfo.RepoInfo 650 Active string 651 Unsupported bool 652 IsImage bool 653 IsVideo bool 654 ContentSrc string 655 BreadCrumbs [][]string 656 ShowRendered bool 657 RenderToggle bool 658 RenderedContents template.HTML 659 types.RepoBlobResponse 660} 661 662func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 663 var style *chroma.Style = styles.Get("catpuccin-latte") 664 665 if params.ShowRendered { 666 switch markup.GetFormat(params.Path) { 667 case markup.FormatMarkdown: 668 p.rctx.RepoInfo = params.RepoInfo 669 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 670 htmlString := p.rctx.RenderMarkdown(params.Contents) 671 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 672 } 673 } 674 675 if params.Lines < 5000 { 676 c := params.Contents 677 formatter := chromahtml.New( 678 chromahtml.InlineCode(false), 679 chromahtml.WithLineNumbers(true), 680 chromahtml.WithLinkableLineNumbers(true, "L"), 681 chromahtml.Standalone(false), 682 chromahtml.WithClasses(true), 683 ) 684 685 lexer := lexers.Get(filepath.Base(params.Path)) 686 if lexer == nil { 687 lexer = lexers.Fallback 688 } 689 690 iterator, err := lexer.Tokenise(nil, c) 691 if err != nil { 692 return fmt.Errorf("chroma tokenize: %w", err) 693 } 694 695 var code bytes.Buffer 696 err = formatter.Format(&code, style, iterator) 697 if err != nil { 698 return fmt.Errorf("chroma format: %w", err) 699 } 700 701 params.Contents = code.String() 702 } 703 704 params.Active = "overview" 705 return p.executeRepo("repo/blob", w, params) 706} 707 708type Collaborator struct { 709 Did string 710 Handle string 711 Role string 712} 713 714type RepoSettingsParams struct { 715 LoggedInUser *oauth.User 716 RepoInfo repoinfo.RepoInfo 717 Collaborators []Collaborator 718 Active string 719 Branches []types.Branch 720 Spindles []string 721 CurrentSpindle string 722 Secrets []*tangled.RepoListSecrets_Secret 723 724 // TODO: use repoinfo.roles 725 IsCollaboratorInviteAllowed bool 726} 727 728func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 729 params.Active = "settings" 730 return p.executeRepo("repo/settings", w, params) 731} 732 733type RepoGeneralSettingsParams struct { 734 LoggedInUser *oauth.User 735 RepoInfo repoinfo.RepoInfo 736 Active string 737 Tabs []map[string]any 738 Tab string 739 Branches []types.Branch 740} 741 742func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 743 params.Active = "settings" 744 return p.executeRepo("repo/settings/general", w, params) 745} 746 747type RepoAccessSettingsParams struct { 748 LoggedInUser *oauth.User 749 RepoInfo repoinfo.RepoInfo 750 Active string 751 Tabs []map[string]any 752 Tab string 753 Collaborators []Collaborator 754} 755 756func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 757 params.Active = "settings" 758 return p.executeRepo("repo/settings/access", w, params) 759} 760 761type RepoPipelineSettingsParams struct { 762 LoggedInUser *oauth.User 763 RepoInfo repoinfo.RepoInfo 764 Active string 765 Tabs []map[string]any 766 Tab string 767 Spindles []string 768 CurrentSpindle string 769 Secrets []map[string]any 770} 771 772func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 773 params.Active = "settings" 774 return p.executeRepo("repo/settings/pipelines", w, params) 775} 776 777type RepoIssuesParams struct { 778 LoggedInUser *oauth.User 779 RepoInfo repoinfo.RepoInfo 780 Active string 781 Issues []db.Issue 782 DidHandleMap map[string]string 783 Page pagination.Page 784 FilteringByOpen bool 785} 786 787func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 788 params.Active = "issues" 789 return p.executeRepo("repo/issues/issues", w, params) 790} 791 792type RepoSingleIssueParams struct { 793 LoggedInUser *oauth.User 794 RepoInfo repoinfo.RepoInfo 795 Active string 796 Issue db.Issue 797 Comments []db.Comment 798 IssueOwnerHandle string 799 DidHandleMap map[string]string 800 801 OrderedReactionKinds []db.ReactionKind 802 Reactions map[db.ReactionKind]int 803 UserReacted map[db.ReactionKind]bool 804 805 State string 806} 807 808type ThreadReactionFragmentParams struct { 809 ThreadAt syntax.ATURI 810 Kind db.ReactionKind 811 Count int 812 IsReacted bool 813} 814 815func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 816 return p.executePlain("repo/fragments/reaction", w, params) 817} 818 819func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 820 params.Active = "issues" 821 if params.Issue.Open { 822 params.State = "open" 823 } else { 824 params.State = "closed" 825 } 826 return p.execute("repo/issues/issue", w, params) 827} 828 829type RepoNewIssueParams struct { 830 LoggedInUser *oauth.User 831 RepoInfo repoinfo.RepoInfo 832 Active string 833} 834 835func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 836 params.Active = "issues" 837 return p.executeRepo("repo/issues/new", w, params) 838} 839 840type EditIssueCommentParams struct { 841 LoggedInUser *oauth.User 842 RepoInfo repoinfo.RepoInfo 843 Issue *db.Issue 844 Comment *db.Comment 845} 846 847func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 848 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 849} 850 851type SingleIssueCommentParams struct { 852 LoggedInUser *oauth.User 853 DidHandleMap map[string]string 854 RepoInfo repoinfo.RepoInfo 855 Issue *db.Issue 856 Comment *db.Comment 857} 858 859func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 860 return p.executePlain("repo/issues/fragments/issueComment", w, params) 861} 862 863type RepoNewPullParams struct { 864 LoggedInUser *oauth.User 865 RepoInfo repoinfo.RepoInfo 866 Branches []types.Branch 867 Strategy string 868 SourceBranch string 869 TargetBranch string 870 Title string 871 Body string 872 Active string 873} 874 875func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 876 params.Active = "pulls" 877 return p.executeRepo("repo/pulls/new", w, params) 878} 879 880type RepoPullsParams struct { 881 LoggedInUser *oauth.User 882 RepoInfo repoinfo.RepoInfo 883 Pulls []*db.Pull 884 Active string 885 DidHandleMap map[string]string 886 FilteringBy db.PullState 887 Stacks map[string]db.Stack 888 Pipelines map[string]db.Pipeline 889} 890 891func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 892 params.Active = "pulls" 893 return p.executeRepo("repo/pulls/pulls", w, params) 894} 895 896type ResubmitResult uint64 897 898const ( 899 ShouldResubmit ResubmitResult = iota 900 ShouldNotResubmit 901 Unknown 902) 903 904func (r ResubmitResult) Yes() bool { 905 return r == ShouldResubmit 906} 907func (r ResubmitResult) No() bool { 908 return r == ShouldNotResubmit 909} 910func (r ResubmitResult) Unknown() bool { 911 return r == Unknown 912} 913 914type RepoSinglePullParams struct { 915 LoggedInUser *oauth.User 916 RepoInfo repoinfo.RepoInfo 917 Active string 918 DidHandleMap map[string]string 919 Pull *db.Pull 920 Stack db.Stack 921 AbandonedPulls []*db.Pull 922 MergeCheck types.MergeCheckResponse 923 ResubmitCheck ResubmitResult 924 Pipelines map[string]db.Pipeline 925 926 OrderedReactionKinds []db.ReactionKind 927 Reactions map[db.ReactionKind]int 928 UserReacted map[db.ReactionKind]bool 929} 930 931func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 932 params.Active = "pulls" 933 return p.executeRepo("repo/pulls/pull", w, params) 934} 935 936type RepoPullPatchParams struct { 937 LoggedInUser *oauth.User 938 DidHandleMap map[string]string 939 RepoInfo repoinfo.RepoInfo 940 Pull *db.Pull 941 Stack db.Stack 942 Diff *types.NiceDiff 943 Round int 944 Submission *db.PullSubmission 945 OrderedReactionKinds []db.ReactionKind 946 DiffOpts types.DiffOpts 947} 948 949// this name is a mouthful 950func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 951 return p.execute("repo/pulls/patch", w, params) 952} 953 954type RepoPullInterdiffParams struct { 955 LoggedInUser *oauth.User 956 DidHandleMap map[string]string 957 RepoInfo repoinfo.RepoInfo 958 Pull *db.Pull 959 Round int 960 Interdiff *patchutil.InterdiffResult 961 OrderedReactionKinds []db.ReactionKind 962 DiffOpts types.DiffOpts 963} 964 965// this name is a mouthful 966func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 967 return p.execute("repo/pulls/interdiff", w, params) 968} 969 970type PullPatchUploadParams struct { 971 RepoInfo repoinfo.RepoInfo 972} 973 974func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 975 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 976} 977 978type PullCompareBranchesParams struct { 979 RepoInfo repoinfo.RepoInfo 980 Branches []types.Branch 981 SourceBranch string 982} 983 984func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 985 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 986} 987 988type PullCompareForkParams struct { 989 RepoInfo repoinfo.RepoInfo 990 Forks []db.Repo 991 Selected string 992} 993 994func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 995 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 996} 997 998type PullCompareForkBranchesParams struct { 999 RepoInfo repoinfo.RepoInfo 1000 SourceBranches []types.Branch 1001 TargetBranches []types.Branch 1002} 1003 1004func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1005 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1006} 1007 1008type PullResubmitParams struct { 1009 LoggedInUser *oauth.User 1010 RepoInfo repoinfo.RepoInfo 1011 Pull *db.Pull 1012 SubmissionId int 1013} 1014 1015func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1016 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1017} 1018 1019type PullActionsParams struct { 1020 LoggedInUser *oauth.User 1021 RepoInfo repoinfo.RepoInfo 1022 Pull *db.Pull 1023 RoundNumber int 1024 MergeCheck types.MergeCheckResponse 1025 ResubmitCheck ResubmitResult 1026 Stack db.Stack 1027} 1028 1029func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1030 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1031} 1032 1033type PullNewCommentParams struct { 1034 LoggedInUser *oauth.User 1035 RepoInfo repoinfo.RepoInfo 1036 Pull *db.Pull 1037 RoundNumber int 1038} 1039 1040func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1041 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1042} 1043 1044type RepoCompareParams struct { 1045 LoggedInUser *oauth.User 1046 RepoInfo repoinfo.RepoInfo 1047 Forks []db.Repo 1048 Branches []types.Branch 1049 Tags []*types.TagReference 1050 Base string 1051 Head string 1052 Diff *types.NiceDiff 1053 DiffOpts types.DiffOpts 1054 1055 Active string 1056} 1057 1058func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1059 params.Active = "overview" 1060 return p.executeRepo("repo/compare/compare", w, params) 1061} 1062 1063type RepoCompareNewParams struct { 1064 LoggedInUser *oauth.User 1065 RepoInfo repoinfo.RepoInfo 1066 Forks []db.Repo 1067 Branches []types.Branch 1068 Tags []*types.TagReference 1069 Base string 1070 Head string 1071 1072 Active string 1073} 1074 1075func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1076 params.Active = "overview" 1077 return p.executeRepo("repo/compare/new", w, params) 1078} 1079 1080type RepoCompareAllowPullParams struct { 1081 LoggedInUser *oauth.User 1082 RepoInfo repoinfo.RepoInfo 1083 Base string 1084 Head string 1085} 1086 1087func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1088 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1089} 1090 1091type RepoCompareDiffParams struct { 1092 LoggedInUser *oauth.User 1093 RepoInfo repoinfo.RepoInfo 1094 Diff types.NiceDiff 1095} 1096 1097func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1098 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1099} 1100 1101type PipelinesParams struct { 1102 LoggedInUser *oauth.User 1103 RepoInfo repoinfo.RepoInfo 1104 Pipelines []db.Pipeline 1105 Active string 1106} 1107 1108func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1109 params.Active = "pipelines" 1110 return p.executeRepo("repo/pipelines/pipelines", w, params) 1111} 1112 1113type LogBlockParams struct { 1114 Id int 1115 Name string 1116 Command string 1117 Collapsed bool 1118} 1119 1120func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1121 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1122} 1123 1124type LogLineParams struct { 1125 Id int 1126 Content string 1127} 1128 1129func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1130 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1131} 1132 1133type WorkflowParams struct { 1134 LoggedInUser *oauth.User 1135 RepoInfo repoinfo.RepoInfo 1136 Pipeline db.Pipeline 1137 Workflow string 1138 LogUrl string 1139 Active string 1140} 1141 1142func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1143 params.Active = "pipelines" 1144 return p.executeRepo("repo/pipelines/workflow", w, params) 1145} 1146 1147func (p *Pages) Static() http.Handler { 1148 if p.dev { 1149 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1150 } 1151 1152 sub, err := fs.Sub(Files, "static") 1153 if err != nil { 1154 log.Fatalf("no static dir found? that's crazy: %v", err) 1155 } 1156 // Custom handler to apply Cache-Control headers for font files 1157 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1158} 1159 1160func Cache(h http.Handler) http.Handler { 1161 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1162 path := strings.Split(r.URL.Path, "?")[0] 1163 1164 if strings.HasSuffix(path, ".css") { 1165 // on day for css files 1166 w.Header().Set("Cache-Control", "public, max-age=86400") 1167 } else { 1168 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1169 } 1170 h.ServeHTTP(w, r) 1171 }) 1172} 1173 1174func CssContentHash() string { 1175 cssFile, err := Files.Open("static/tw.css") 1176 if err != nil { 1177 log.Printf("Error opening CSS file: %v", err) 1178 return "" 1179 } 1180 defer cssFile.Close() 1181 1182 hasher := sha256.New() 1183 if _, err := io.Copy(hasher, cssFile); err != nil { 1184 log.Printf("Error hashing CSS file: %v", err) 1185 return "" 1186 } 1187 1188 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1189} 1190 1191func (p *Pages) Error500(w io.Writer) error { 1192 return p.execute("errors/500", w, nil) 1193} 1194 1195func (p *Pages) Error404(w io.Writer) error { 1196 return p.execute("errors/404", w, nil) 1197} 1198 1199func (p *Pages) Error503(w io.Writer) error { 1200 return p.execute("errors/503", w, nil) 1201}