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