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 265type SignupParams struct{} 266 267func (p *Pages) CompleteSignup(w io.Writer, params SignupParams) error { 268 return p.executePlain("user/completeSignup", w, params) 269} 270 271type TermsOfServiceParams struct { 272 LoggedInUser *oauth.User 273} 274 275func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 276 return p.execute("legal/terms", w, params) 277} 278 279type PrivacyPolicyParams struct { 280 LoggedInUser *oauth.User 281} 282 283func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 284 return p.execute("legal/privacy", w, params) 285} 286 287type TimelineParams struct { 288 LoggedInUser *oauth.User 289 Timeline []db.TimelineEvent 290 DidHandleMap map[string]string 291} 292 293func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 294 return p.execute("timeline", w, params) 295} 296 297type SettingsParams struct { 298 LoggedInUser *oauth.User 299 PubKeys []db.PublicKey 300 Emails []db.Email 301} 302 303func (p *Pages) Settings(w io.Writer, params SettingsParams) error { 304 return p.execute("settings", w, params) 305} 306 307type KnotsParams struct { 308 LoggedInUser *oauth.User 309 Registrations []db.Registration 310} 311 312func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 313 return p.execute("knots/index", w, params) 314} 315 316type KnotParams struct { 317 LoggedInUser *oauth.User 318 DidHandleMap map[string]string 319 Registration *db.Registration 320 Members []string 321 Repos map[string][]db.Repo 322 IsOwner bool 323} 324 325func (p *Pages) Knot(w io.Writer, params KnotParams) error { 326 return p.execute("knots/dashboard", w, params) 327} 328 329type KnotListingParams struct { 330 db.Registration 331} 332 333func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 334 return p.executePlain("knots/fragments/knotListing", w, params) 335} 336 337type KnotListingFullParams struct { 338 Registrations []db.Registration 339} 340 341func (p *Pages) KnotListingFull(w io.Writer, params KnotListingFullParams) error { 342 return p.executePlain("knots/fragments/knotListingFull", w, params) 343} 344 345type KnotSecretParams struct { 346 Secret string 347} 348 349func (p *Pages) KnotSecret(w io.Writer, params KnotSecretParams) error { 350 return p.executePlain("knots/fragments/secret", w, params) 351} 352 353type SpindlesParams struct { 354 LoggedInUser *oauth.User 355 Spindles []db.Spindle 356} 357 358func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 359 return p.execute("spindles/index", w, params) 360} 361 362type SpindleListingParams struct { 363 db.Spindle 364} 365 366func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 367 return p.executePlain("spindles/fragments/spindleListing", w, params) 368} 369 370type SpindleDashboardParams struct { 371 LoggedInUser *oauth.User 372 Spindle db.Spindle 373 Members []string 374 Repos map[string][]db.Repo 375 DidHandleMap map[string]string 376} 377 378func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 379 return p.execute("spindles/dashboard", w, params) 380} 381 382type NewRepoParams struct { 383 LoggedInUser *oauth.User 384 Knots []string 385} 386 387func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 388 return p.execute("repo/new", w, params) 389} 390 391type ForkRepoParams struct { 392 LoggedInUser *oauth.User 393 Knots []string 394 RepoInfo repoinfo.RepoInfo 395} 396 397func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 398 return p.execute("repo/fork", w, params) 399} 400 401type ProfilePageParams struct { 402 LoggedInUser *oauth.User 403 Repos []db.Repo 404 CollaboratingRepos []db.Repo 405 ProfileTimeline *db.ProfileTimeline 406 Card ProfileCard 407 Punchcard db.Punchcard 408 409 DidHandleMap map[string]string 410} 411 412type ProfileCard struct { 413 UserDid string 414 UserHandle string 415 FollowStatus db.FollowStatus 416 AvatarUri string 417 Followers int 418 Following int 419 420 Profile *db.Profile 421} 422 423func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 424 return p.execute("user/profile", w, params) 425} 426 427type ReposPageParams struct { 428 LoggedInUser *oauth.User 429 Repos []db.Repo 430 Card ProfileCard 431 432 DidHandleMap map[string]string 433} 434 435func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 436 return p.execute("user/repos", w, params) 437} 438 439type FollowFragmentParams struct { 440 UserDid string 441 FollowStatus db.FollowStatus 442} 443 444func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 445 return p.executePlain("user/fragments/follow", w, params) 446} 447 448type EditBioParams struct { 449 LoggedInUser *oauth.User 450 Profile *db.Profile 451} 452 453func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 454 return p.executePlain("user/fragments/editBio", w, params) 455} 456 457type EditPinsParams struct { 458 LoggedInUser *oauth.User 459 Profile *db.Profile 460 AllRepos []PinnedRepo 461 DidHandleMap map[string]string 462} 463 464type PinnedRepo struct { 465 IsPinned bool 466 db.Repo 467} 468 469func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 470 return p.executePlain("user/fragments/editPins", w, params) 471} 472 473type RepoStarFragmentParams struct { 474 IsStarred bool 475 RepoAt syntax.ATURI 476 Stats db.RepoStats 477} 478 479func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error { 480 return p.executePlain("repo/fragments/repoStar", w, params) 481} 482 483type RepoDescriptionParams struct { 484 RepoInfo repoinfo.RepoInfo 485} 486 487func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 488 return p.executePlain("repo/fragments/editRepoDescription", w, params) 489} 490 491func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 492 return p.executePlain("repo/fragments/repoDescription", w, params) 493} 494 495type RepoIndexParams struct { 496 LoggedInUser *oauth.User 497 RepoInfo repoinfo.RepoInfo 498 Active string 499 TagMap map[string][]string 500 CommitsTrunc []*object.Commit 501 TagsTrunc []*types.TagReference 502 BranchesTrunc []types.Branch 503 ForkInfo *types.ForkInfo 504 HTMLReadme template.HTML 505 Raw bool 506 EmailToDidOrHandle map[string]string 507 VerifiedCommits commitverify.VerifiedCommits 508 Languages []types.RepoLanguageDetails 509 Pipelines map[string]db.Pipeline 510 types.RepoIndexResponse 511} 512 513func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 514 params.Active = "overview" 515 if params.IsEmpty { 516 return p.executeRepo("repo/empty", w, params) 517 } 518 519 p.rctx.RepoInfo = params.RepoInfo 520 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 521 522 if params.ReadmeFileName != "" { 523 var htmlString string 524 ext := filepath.Ext(params.ReadmeFileName) 525 switch ext { 526 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 527 htmlString = p.rctx.Sanitize(htmlString) 528 htmlString = p.rctx.RenderMarkdown(params.Readme) 529 params.Raw = false 530 params.HTMLReadme = template.HTML(htmlString) 531 default: 532 params.Raw = true 533 } 534 } 535 536 return p.executeRepo("repo/index", w, params) 537} 538 539type RepoLogParams struct { 540 LoggedInUser *oauth.User 541 RepoInfo repoinfo.RepoInfo 542 TagMap map[string][]string 543 types.RepoLogResponse 544 Active string 545 EmailToDidOrHandle map[string]string 546 VerifiedCommits commitverify.VerifiedCommits 547 Pipelines map[string]db.Pipeline 548} 549 550func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 551 params.Active = "overview" 552 return p.executeRepo("repo/log", w, params) 553} 554 555type RepoCommitParams struct { 556 LoggedInUser *oauth.User 557 RepoInfo repoinfo.RepoInfo 558 Active string 559 EmailToDidOrHandle map[string]string 560 Pipeline *db.Pipeline 561 DiffOpts types.DiffOpts 562 563 // singular because it's always going to be just one 564 VerifiedCommit commitverify.VerifiedCommits 565 566 types.RepoCommitResponse 567} 568 569func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 570 params.Active = "overview" 571 return p.executeRepo("repo/commit", w, params) 572} 573 574type RepoTreeParams struct { 575 LoggedInUser *oauth.User 576 RepoInfo repoinfo.RepoInfo 577 Active string 578 BreadCrumbs [][]string 579 TreePath string 580 types.RepoTreeResponse 581} 582 583type RepoTreeStats struct { 584 NumFolders uint64 585 NumFiles uint64 586} 587 588func (r RepoTreeParams) TreeStats() RepoTreeStats { 589 numFolders, numFiles := 0, 0 590 for _, f := range r.Files { 591 if !f.IsFile { 592 numFolders += 1 593 } else if f.IsFile { 594 numFiles += 1 595 } 596 } 597 598 return RepoTreeStats{ 599 NumFolders: uint64(numFolders), 600 NumFiles: uint64(numFiles), 601 } 602} 603 604func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 605 params.Active = "overview" 606 return p.execute("repo/tree", w, params) 607} 608 609type RepoBranchesParams struct { 610 LoggedInUser *oauth.User 611 RepoInfo repoinfo.RepoInfo 612 Active string 613 types.RepoBranchesResponse 614} 615 616func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 617 params.Active = "overview" 618 return p.executeRepo("repo/branches", w, params) 619} 620 621type RepoTagsParams struct { 622 LoggedInUser *oauth.User 623 RepoInfo repoinfo.RepoInfo 624 Active string 625 types.RepoTagsResponse 626 ArtifactMap map[plumbing.Hash][]db.Artifact 627 DanglingArtifacts []db.Artifact 628} 629 630func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 631 params.Active = "overview" 632 return p.executeRepo("repo/tags", w, params) 633} 634 635type RepoArtifactParams struct { 636 LoggedInUser *oauth.User 637 RepoInfo repoinfo.RepoInfo 638 Artifact db.Artifact 639} 640 641func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 642 return p.executePlain("repo/fragments/artifact", w, params) 643} 644 645type RepoBlobParams struct { 646 LoggedInUser *oauth.User 647 RepoInfo repoinfo.RepoInfo 648 Active string 649 Unsupported bool 650 IsImage bool 651 IsVideo bool 652 ContentSrc string 653 BreadCrumbs [][]string 654 ShowRendered bool 655 RenderToggle bool 656 RenderedContents template.HTML 657 types.RepoBlobResponse 658} 659 660func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 661 var style *chroma.Style = styles.Get("catpuccin-latte") 662 663 if params.ShowRendered { 664 switch markup.GetFormat(params.Path) { 665 case markup.FormatMarkdown: 666 p.rctx.RepoInfo = params.RepoInfo 667 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 668 htmlString := p.rctx.RenderMarkdown(params.Contents) 669 params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 670 } 671 } 672 673 if params.Lines < 5000 { 674 c := params.Contents 675 formatter := chromahtml.New( 676 chromahtml.InlineCode(false), 677 chromahtml.WithLineNumbers(true), 678 chromahtml.WithLinkableLineNumbers(true, "L"), 679 chromahtml.Standalone(false), 680 chromahtml.WithClasses(true), 681 ) 682 683 lexer := lexers.Get(filepath.Base(params.Path)) 684 if lexer == nil { 685 lexer = lexers.Fallback 686 } 687 688 iterator, err := lexer.Tokenise(nil, c) 689 if err != nil { 690 return fmt.Errorf("chroma tokenize: %w", err) 691 } 692 693 var code bytes.Buffer 694 err = formatter.Format(&code, style, iterator) 695 if err != nil { 696 return fmt.Errorf("chroma format: %w", err) 697 } 698 699 params.Contents = code.String() 700 } 701 702 params.Active = "overview" 703 return p.executeRepo("repo/blob", w, params) 704} 705 706type Collaborator struct { 707 Did string 708 Handle string 709 Role string 710} 711 712type RepoSettingsParams struct { 713 LoggedInUser *oauth.User 714 RepoInfo repoinfo.RepoInfo 715 Collaborators []Collaborator 716 Active string 717 Branches []types.Branch 718 Spindles []string 719 CurrentSpindle string 720 Secrets []*tangled.RepoListSecrets_Secret 721 722 // TODO: use repoinfo.roles 723 IsCollaboratorInviteAllowed bool 724} 725 726func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 727 params.Active = "settings" 728 return p.executeRepo("repo/settings", w, params) 729} 730 731type RepoGeneralSettingsParams struct { 732 LoggedInUser *oauth.User 733 RepoInfo repoinfo.RepoInfo 734 Active string 735 Tabs []map[string]any 736 Tab string 737 Branches []types.Branch 738} 739 740func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 741 params.Active = "settings" 742 return p.executeRepo("repo/settings/general", w, params) 743} 744 745type RepoAccessSettingsParams struct { 746 LoggedInUser *oauth.User 747 RepoInfo repoinfo.RepoInfo 748 Active string 749 Tabs []map[string]any 750 Tab string 751 Collaborators []Collaborator 752} 753 754func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 755 params.Active = "settings" 756 return p.executeRepo("repo/settings/access", w, params) 757} 758 759type RepoPipelineSettingsParams struct { 760 LoggedInUser *oauth.User 761 RepoInfo repoinfo.RepoInfo 762 Active string 763 Tabs []map[string]any 764 Tab string 765 Spindles []string 766 CurrentSpindle string 767 Secrets []map[string]any 768} 769 770func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 771 params.Active = "settings" 772 return p.executeRepo("repo/settings/pipelines", w, params) 773} 774 775type RepoIssuesParams struct { 776 LoggedInUser *oauth.User 777 RepoInfo repoinfo.RepoInfo 778 Active string 779 Issues []db.Issue 780 DidHandleMap map[string]string 781 Page pagination.Page 782 FilteringByOpen bool 783} 784 785func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 786 params.Active = "issues" 787 return p.executeRepo("repo/issues/issues", w, params) 788} 789 790type RepoSingleIssueParams struct { 791 LoggedInUser *oauth.User 792 RepoInfo repoinfo.RepoInfo 793 Active string 794 Issue db.Issue 795 Comments []db.Comment 796 IssueOwnerHandle string 797 DidHandleMap map[string]string 798 799 OrderedReactionKinds []db.ReactionKind 800 Reactions map[db.ReactionKind]int 801 UserReacted map[db.ReactionKind]bool 802 803 State string 804} 805 806type ThreadReactionFragmentParams struct { 807 ThreadAt syntax.ATURI 808 Kind db.ReactionKind 809 Count int 810 IsReacted bool 811} 812 813func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 814 return p.executePlain("repo/fragments/reaction", w, params) 815} 816 817func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 818 params.Active = "issues" 819 if params.Issue.Open { 820 params.State = "open" 821 } else { 822 params.State = "closed" 823 } 824 return p.execute("repo/issues/issue", w, params) 825} 826 827type RepoNewIssueParams struct { 828 LoggedInUser *oauth.User 829 RepoInfo repoinfo.RepoInfo 830 Active string 831} 832 833func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 834 params.Active = "issues" 835 return p.executeRepo("repo/issues/new", w, params) 836} 837 838type EditIssueCommentParams struct { 839 LoggedInUser *oauth.User 840 RepoInfo repoinfo.RepoInfo 841 Issue *db.Issue 842 Comment *db.Comment 843} 844 845func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 846 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 847} 848 849type SingleIssueCommentParams struct { 850 LoggedInUser *oauth.User 851 DidHandleMap map[string]string 852 RepoInfo repoinfo.RepoInfo 853 Issue *db.Issue 854 Comment *db.Comment 855} 856 857func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 858 return p.executePlain("repo/issues/fragments/issueComment", w, params) 859} 860 861type RepoNewPullParams struct { 862 LoggedInUser *oauth.User 863 RepoInfo repoinfo.RepoInfo 864 Branches []types.Branch 865 Strategy string 866 SourceBranch string 867 TargetBranch string 868 Title string 869 Body string 870 Active string 871} 872 873func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 874 params.Active = "pulls" 875 return p.executeRepo("repo/pulls/new", w, params) 876} 877 878type RepoPullsParams struct { 879 LoggedInUser *oauth.User 880 RepoInfo repoinfo.RepoInfo 881 Pulls []*db.Pull 882 Active string 883 DidHandleMap map[string]string 884 FilteringBy db.PullState 885 Stacks map[string]db.Stack 886 Pipelines map[string]db.Pipeline 887} 888 889func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 890 params.Active = "pulls" 891 return p.executeRepo("repo/pulls/pulls", w, params) 892} 893 894type ResubmitResult uint64 895 896const ( 897 ShouldResubmit ResubmitResult = iota 898 ShouldNotResubmit 899 Unknown 900) 901 902func (r ResubmitResult) Yes() bool { 903 return r == ShouldResubmit 904} 905func (r ResubmitResult) No() bool { 906 return r == ShouldNotResubmit 907} 908func (r ResubmitResult) Unknown() bool { 909 return r == Unknown 910} 911 912type RepoSinglePullParams struct { 913 LoggedInUser *oauth.User 914 RepoInfo repoinfo.RepoInfo 915 Active string 916 DidHandleMap map[string]string 917 Pull *db.Pull 918 Stack db.Stack 919 AbandonedPulls []*db.Pull 920 MergeCheck types.MergeCheckResponse 921 ResubmitCheck ResubmitResult 922 Pipelines map[string]db.Pipeline 923 924 OrderedReactionKinds []db.ReactionKind 925 Reactions map[db.ReactionKind]int 926 UserReacted map[db.ReactionKind]bool 927} 928 929func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 930 params.Active = "pulls" 931 return p.executeRepo("repo/pulls/pull", w, params) 932} 933 934type RepoPullPatchParams struct { 935 LoggedInUser *oauth.User 936 DidHandleMap map[string]string 937 RepoInfo repoinfo.RepoInfo 938 Pull *db.Pull 939 Stack db.Stack 940 Diff *types.NiceDiff 941 Round int 942 Submission *db.PullSubmission 943 OrderedReactionKinds []db.ReactionKind 944 DiffOpts types.DiffOpts 945} 946 947// this name is a mouthful 948func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 949 return p.execute("repo/pulls/patch", w, params) 950} 951 952type RepoPullInterdiffParams struct { 953 LoggedInUser *oauth.User 954 DidHandleMap map[string]string 955 RepoInfo repoinfo.RepoInfo 956 Pull *db.Pull 957 Round int 958 Interdiff *patchutil.InterdiffResult 959 OrderedReactionKinds []db.ReactionKind 960 DiffOpts types.DiffOpts 961} 962 963// this name is a mouthful 964func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 965 return p.execute("repo/pulls/interdiff", w, params) 966} 967 968type PullPatchUploadParams struct { 969 RepoInfo repoinfo.RepoInfo 970} 971 972func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 973 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 974} 975 976type PullCompareBranchesParams struct { 977 RepoInfo repoinfo.RepoInfo 978 Branches []types.Branch 979 SourceBranch string 980} 981 982func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 983 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 984} 985 986type PullCompareForkParams struct { 987 RepoInfo repoinfo.RepoInfo 988 Forks []db.Repo 989 Selected string 990} 991 992func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 993 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 994} 995 996type PullCompareForkBranchesParams struct { 997 RepoInfo repoinfo.RepoInfo 998 SourceBranches []types.Branch 999 TargetBranches []types.Branch 1000} 1001 1002func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1003 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1004} 1005 1006type PullResubmitParams struct { 1007 LoggedInUser *oauth.User 1008 RepoInfo repoinfo.RepoInfo 1009 Pull *db.Pull 1010 SubmissionId int 1011} 1012 1013func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1014 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1015} 1016 1017type PullActionsParams struct { 1018 LoggedInUser *oauth.User 1019 RepoInfo repoinfo.RepoInfo 1020 Pull *db.Pull 1021 RoundNumber int 1022 MergeCheck types.MergeCheckResponse 1023 ResubmitCheck ResubmitResult 1024 Stack db.Stack 1025} 1026 1027func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1028 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1029} 1030 1031type PullNewCommentParams struct { 1032 LoggedInUser *oauth.User 1033 RepoInfo repoinfo.RepoInfo 1034 Pull *db.Pull 1035 RoundNumber int 1036} 1037 1038func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1039 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1040} 1041 1042type RepoCompareParams struct { 1043 LoggedInUser *oauth.User 1044 RepoInfo repoinfo.RepoInfo 1045 Forks []db.Repo 1046 Branches []types.Branch 1047 Tags []*types.TagReference 1048 Base string 1049 Head string 1050 Diff *types.NiceDiff 1051 DiffOpts types.DiffOpts 1052 1053 Active string 1054} 1055 1056func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1057 params.Active = "overview" 1058 return p.executeRepo("repo/compare/compare", w, params) 1059} 1060 1061type RepoCompareNewParams struct { 1062 LoggedInUser *oauth.User 1063 RepoInfo repoinfo.RepoInfo 1064 Forks []db.Repo 1065 Branches []types.Branch 1066 Tags []*types.TagReference 1067 Base string 1068 Head string 1069 1070 Active string 1071} 1072 1073func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1074 params.Active = "overview" 1075 return p.executeRepo("repo/compare/new", w, params) 1076} 1077 1078type RepoCompareAllowPullParams struct { 1079 LoggedInUser *oauth.User 1080 RepoInfo repoinfo.RepoInfo 1081 Base string 1082 Head string 1083} 1084 1085func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1086 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1087} 1088 1089type RepoCompareDiffParams struct { 1090 LoggedInUser *oauth.User 1091 RepoInfo repoinfo.RepoInfo 1092 Diff types.NiceDiff 1093} 1094 1095func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1096 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1097} 1098 1099type PipelinesParams struct { 1100 LoggedInUser *oauth.User 1101 RepoInfo repoinfo.RepoInfo 1102 Pipelines []db.Pipeline 1103 Active string 1104} 1105 1106func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1107 params.Active = "pipelines" 1108 return p.executeRepo("repo/pipelines/pipelines", w, params) 1109} 1110 1111type LogBlockParams struct { 1112 Id int 1113 Name string 1114 Command string 1115 Collapsed bool 1116} 1117 1118func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1119 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1120} 1121 1122type LogLineParams struct { 1123 Id int 1124 Content string 1125} 1126 1127func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1128 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1129} 1130 1131type WorkflowParams struct { 1132 LoggedInUser *oauth.User 1133 RepoInfo repoinfo.RepoInfo 1134 Pipeline db.Pipeline 1135 Workflow string 1136 LogUrl string 1137 Active string 1138} 1139 1140func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1141 params.Active = "pipelines" 1142 return p.executeRepo("repo/pipelines/workflow", w, params) 1143} 1144 1145func (p *Pages) Static() http.Handler { 1146 if p.dev { 1147 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1148 } 1149 1150 sub, err := fs.Sub(Files, "static") 1151 if err != nil { 1152 log.Fatalf("no static dir found? that's crazy: %v", err) 1153 } 1154 // Custom handler to apply Cache-Control headers for font files 1155 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1156} 1157 1158func Cache(h http.Handler) http.Handler { 1159 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1160 path := strings.Split(r.URL.Path, "?")[0] 1161 1162 if strings.HasSuffix(path, ".css") { 1163 // on day for css files 1164 w.Header().Set("Cache-Control", "public, max-age=86400") 1165 } else { 1166 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1167 } 1168 h.ServeHTTP(w, r) 1169 }) 1170} 1171 1172func CssContentHash() string { 1173 cssFile, err := Files.Open("static/tw.css") 1174 if err != nil { 1175 log.Printf("Error opening CSS file: %v", err) 1176 return "" 1177 } 1178 defer cssFile.Close() 1179 1180 hasher := sha256.New() 1181 if _, err := io.Copy(hasher, cssFile); err != nil { 1182 log.Printf("Error hashing CSS file: %v", err) 1183 return "" 1184 } 1185 1186 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1187} 1188 1189func (p *Pages) Error500(w io.Writer) error { 1190 return p.execute("errors/500", w, nil) 1191} 1192 1193func (p *Pages) Error404(w io.Writer) error { 1194 return p.execute("errors/404", w, nil) 1195} 1196 1197func (p *Pages) Error503(w io.Writer) error { 1198 return p.execute("errors/503", w, nil) 1199}