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