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