Monorepo for Tangled
at a120ed6bdd9785413c5fb3eacb13a56eef410c66 1480 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/db" 23 "tangled.org/core/appview/models" 24 "tangled.org/core/appview/oauth" 25 "tangled.org/core/appview/pages/markup" 26 "tangled.org/core/appview/pages/repoinfo" 27 "tangled.org/core/appview/pagination" 28 "tangled.org/core/idresolver" 29 "tangled.org/core/patchutil" 30 "tangled.org/core/types" 31 32 "github.com/bluesky-social/indigo/atproto/identity" 33 "github.com/bluesky-social/indigo/atproto/syntax" 34 "github.com/go-git/go-git/v5/plumbing" 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 db *db.DB 47 dev bool 48 embedFS fs.FS 49 templateDir string // Path to templates on disk for dev mode 50 rctx *markup.RenderContext 51 logger *slog.Logger 52} 53 54func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages { 55 // initialized with safe defaults, can be overriden per use 56 rctx := &markup.RenderContext{ 57 IsDev: config.Core.Dev, 58 CamoUrl: config.Camo.Host, 59 CamoSecret: config.Camo.SharedSecret, 60 Sanitizer: markup.NewSanitizer(), 61 Files: Files, 62 } 63 64 p := &Pages{ 65 mu: sync.RWMutex{}, 66 cache: NewTmplCache[string, *template.Template](), 67 dev: config.Core.Dev, 68 avatar: config.Avatar, 69 rctx: rctx, 70 resolver: res, 71 db: database, 72 templateDir: "appview/pages", 73 logger: logger, 74 } 75 76 if p.dev { 77 p.embedFS = os.DirFS(p.templateDir) 78 } else { 79 p.embedFS = Files 80 } 81 82 return p 83} 84 85// reverse of pathToName 86func (p *Pages) nameToPath(s string) string { 87 return "templates/" + s + ".html" 88} 89 90func (p *Pages) fragmentPaths() ([]string, error) { 91 var fragmentPaths []string 92 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 93 if err != nil { 94 return err 95 } 96 if d.IsDir() { 97 return nil 98 } 99 if !strings.HasSuffix(path, ".html") { 100 return nil 101 } 102 if !strings.Contains(path, "fragments/") { 103 return nil 104 } 105 fragmentPaths = append(fragmentPaths, path) 106 return nil 107 }) 108 if err != nil { 109 return nil, err 110 } 111 112 return fragmentPaths, nil 113} 114 115// parse without memoization 116func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 117 paths, err := p.fragmentPaths() 118 if err != nil { 119 return nil, err 120 } 121 for _, s := range stack { 122 paths = append(paths, p.nameToPath(s)) 123 } 124 125 funcs := p.funcMap() 126 top := stack[len(stack)-1] 127 parsed, err := template.New(top). 128 Funcs(funcs). 129 ParseFS(p.embedFS, paths...) 130 if err != nil { 131 return nil, err 132 } 133 134 return parsed, nil 135} 136 137func (p *Pages) parse(stack ...string) (*template.Template, error) { 138 key := strings.Join(stack, "|") 139 140 // never cache in dev mode 141 if cached, exists := p.cache.Get(key); !p.dev && exists { 142 return cached, nil 143 } 144 145 result, err := p.rawParse(stack...) 146 if err != nil { 147 return nil, err 148 } 149 150 p.cache.Set(key, result) 151 return result, nil 152} 153 154func (p *Pages) parseBase(top string) (*template.Template, error) { 155 stack := []string{ 156 "layouts/base", 157 top, 158 } 159 return p.parse(stack...) 160} 161 162func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 163 stack := []string{ 164 "layouts/base", 165 "layouts/repobase", 166 top, 167 } 168 return p.parse(stack...) 169} 170 171func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 172 stack := []string{ 173 "layouts/base", 174 "layouts/profilebase", 175 top, 176 } 177 return p.parse(stack...) 178} 179 180func (p *Pages) executePlain(name string, w io.Writer, params any) error { 181 tpl, err := p.parse(name) 182 if err != nil { 183 return err 184 } 185 186 return tpl.Execute(w, params) 187} 188 189func (p *Pages) execute(name string, w io.Writer, params any) error { 190 tpl, err := p.parseBase(name) 191 if err != nil { 192 return err 193 } 194 195 return tpl.ExecuteTemplate(w, "layouts/base", params) 196} 197 198func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 199 tpl, err := p.parseRepoBase(name) 200 if err != nil { 201 return err 202 } 203 204 return tpl.ExecuteTemplate(w, "layouts/base", params) 205} 206 207func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 208 tpl, err := p.parseProfileBase(name) 209 if err != nil { 210 return err 211 } 212 213 return tpl.ExecuteTemplate(w, "layouts/base", params) 214} 215 216type DollyParams struct { 217 Classes string 218 FillColor string 219} 220 221func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 222 return p.executePlain("fragments/dolly/logo", w, params) 223} 224 225func (p *Pages) Favicon(w io.Writer) error { 226 return p.Dolly(w, DollyParams{ 227 Classes: "text-black dark:text-white", 228 }) 229} 230 231type LoginParams struct { 232 ReturnUrl string 233 ErrorCode string 234 AddAccount bool 235 LoggedInUser *oauth.MultiAccountUser 236} 237 238func (p *Pages) Login(w io.Writer, params LoginParams) error { 239 return p.executePlain("user/login", w, params) 240} 241 242type SignupParams struct { 243 CloudflareSiteKey string 244} 245 246func (p *Pages) Signup(w io.Writer, params SignupParams) error { 247 return p.executePlain("user/signup", w, params) 248} 249 250func (p *Pages) CompleteSignup(w io.Writer) error { 251 return p.executePlain("user/completeSignup", w, nil) 252} 253 254type TermsOfServiceParams struct { 255 LoggedInUser *oauth.MultiAccountUser 256 Content template.HTML 257} 258 259func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 260 filename := "terms.md" 261 filePath := filepath.Join("legal", filename) 262 263 file, err := p.embedFS.Open(filePath) 264 if err != nil { 265 return fmt.Errorf("failed to read %s: %w", filename, err) 266 } 267 defer file.Close() 268 269 markdownBytes, err := io.ReadAll(file) 270 if err != nil { 271 return fmt.Errorf("failed to read %s: %w", filename, err) 272 } 273 274 p.rctx.RendererType = markup.RendererTypeDefault 275 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 276 sanitized := p.rctx.SanitizeDefault(htmlString) 277 params.Content = template.HTML(sanitized) 278 279 return p.execute("legal/terms", w, params) 280} 281 282type PrivacyPolicyParams struct { 283 LoggedInUser *oauth.MultiAccountUser 284 Content template.HTML 285} 286 287func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 288 filename := "privacy.md" 289 filePath := filepath.Join("legal", filename) 290 291 file, err := p.embedFS.Open(filePath) 292 if err != nil { 293 return fmt.Errorf("failed to read %s: %w", filename, err) 294 } 295 defer file.Close() 296 297 markdownBytes, err := io.ReadAll(file) 298 if err != nil { 299 return fmt.Errorf("failed to read %s: %w", filename, err) 300 } 301 302 p.rctx.RendererType = markup.RendererTypeDefault 303 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 304 sanitized := p.rctx.SanitizeDefault(htmlString) 305 params.Content = template.HTML(sanitized) 306 307 return p.execute("legal/privacy", w, params) 308} 309 310type BrandParams struct { 311 LoggedInUser *oauth.MultiAccountUser 312} 313 314func (p *Pages) Brand(w io.Writer, params BrandParams) error { 315 return p.execute("brand/brand", w, params) 316} 317 318type TimelineParams struct { 319 LoggedInUser *oauth.MultiAccountUser 320 Timeline []models.TimelineEvent 321 Repos []models.Repo 322 GfiLabel *models.LabelDefinition 323} 324 325func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 326 return p.execute("timeline/timeline", w, params) 327} 328 329type GoodFirstIssuesParams struct { 330 LoggedInUser *oauth.MultiAccountUser 331 Issues []models.Issue 332 RepoGroups []*models.RepoGroup 333 LabelDefs map[string]*models.LabelDefinition 334 GfiLabel *models.LabelDefinition 335 Page pagination.Page 336} 337 338func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 339 return p.execute("goodfirstissues/index", w, params) 340} 341 342type UserProfileSettingsParams struct { 343 LoggedInUser *oauth.MultiAccountUser 344 Tab string 345} 346 347func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 348 params.Tab = "profile" 349 return p.execute("user/settings/profile", w, params) 350} 351 352type NotificationsParams struct { 353 LoggedInUser *oauth.MultiAccountUser 354 Notifications []*models.NotificationWithEntity 355 UnreadCount int 356 Page pagination.Page 357 Total int64 358} 359 360func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 361 return p.execute("notifications/list", w, params) 362} 363 364type NotificationItemParams struct { 365 Notification *models.Notification 366} 367 368func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 369 return p.executePlain("notifications/fragments/item", w, params) 370} 371 372type NotificationCountParams struct { 373 Count int64 374} 375 376func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 377 return p.executePlain("notifications/fragments/count", w, params) 378} 379 380type UserKeysSettingsParams struct { 381 LoggedInUser *oauth.MultiAccountUser 382 PubKeys []models.PublicKey 383 Tab string 384} 385 386func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 387 params.Tab = "keys" 388 return p.execute("user/settings/keys", w, params) 389} 390 391type UserEmailsSettingsParams struct { 392 LoggedInUser *oauth.MultiAccountUser 393 Emails []models.Email 394 Tab string 395} 396 397func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 398 params.Tab = "emails" 399 return p.execute("user/settings/emails", w, params) 400} 401 402type UserNotificationSettingsParams struct { 403 LoggedInUser *oauth.MultiAccountUser 404 Preferences *models.NotificationPreferences 405 Tab string 406} 407 408func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 409 params.Tab = "notifications" 410 return p.execute("user/settings/notifications", w, params) 411} 412 413type UpgradeBannerParams struct { 414 Registrations []models.Registration 415 Spindles []models.Spindle 416} 417 418func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 419 return p.executePlain("banner", w, params) 420} 421 422type KnotsParams struct { 423 LoggedInUser *oauth.MultiAccountUser 424 Registrations []models.Registration 425 Tab string 426} 427 428func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 429 params.Tab = "knots" 430 return p.execute("knots/index", w, params) 431} 432 433type KnotParams struct { 434 LoggedInUser *oauth.MultiAccountUser 435 Registration *models.Registration 436 Members []string 437 Repos map[string][]models.Repo 438 IsOwner bool 439 Tab string 440} 441 442func (p *Pages) Knot(w io.Writer, params KnotParams) error { 443 return p.execute("knots/dashboard", w, params) 444} 445 446type KnotListingParams struct { 447 *models.Registration 448} 449 450func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 451 return p.executePlain("knots/fragments/knotListing", w, params) 452} 453 454type SpindlesParams struct { 455 LoggedInUser *oauth.MultiAccountUser 456 Spindles []models.Spindle 457 Tab string 458} 459 460func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 461 params.Tab = "spindles" 462 return p.execute("spindles/index", w, params) 463} 464 465type SpindleListingParams struct { 466 models.Spindle 467 Tab string 468} 469 470func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 471 return p.executePlain("spindles/fragments/spindleListing", w, params) 472} 473 474type SpindleDashboardParams struct { 475 LoggedInUser *oauth.MultiAccountUser 476 Spindle models.Spindle 477 Members []string 478 Repos map[string][]models.Repo 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 Tab string 890 Branches []types.Branch 891} 892 893func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 894 params.Active = "settings" 895 params.Tab = "general" 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 Tab string 904 Collaborators []Collaborator 905} 906 907func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 908 params.Active = "settings" 909 params.Tab = "access" 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 Tab string 918 Spindles []string 919 CurrentSpindle string 920 Secrets []map[string]any 921} 922 923func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 924 params.Active = "settings" 925 params.Tab = "pipelines" 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 Reactions map[models.ReactionKind]models.ReactionDisplayData 956 UserReacted map[models.ReactionKind]bool 957} 958 959func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 960 params.Active = "issues" 961 return p.executeRepo("repo/issues/issue", w, params) 962} 963 964type EditIssueParams struct { 965 LoggedInUser *oauth.MultiAccountUser 966 RepoInfo repoinfo.RepoInfo 967 Issue *models.Issue 968 Action string 969} 970 971func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 972 params.Action = "edit" 973 return p.executePlain("repo/issues/fragments/putIssue", w, params) 974} 975 976type ThreadReactionFragmentParams struct { 977 ThreadAt syntax.ATURI 978 Kind models.ReactionKind 979 Count int 980 Users []string 981 IsReacted bool 982} 983 984func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 985 return p.executePlain("repo/fragments/reaction", w, params) 986} 987 988type RepoNewIssueParams struct { 989 LoggedInUser *oauth.MultiAccountUser 990 RepoInfo repoinfo.RepoInfo 991 Issue *models.Issue // existing issue if any -- passed when editing 992 Active string 993 Action string 994} 995 996func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 997 params.Active = "issues" 998 params.Action = "create" 999 return p.executeRepo("repo/issues/new", w, params) 1000} 1001 1002type EditIssueCommentParams struct { 1003 LoggedInUser *oauth.MultiAccountUser 1004 RepoInfo repoinfo.RepoInfo 1005 Issue *models.Issue 1006 Comment *models.IssueComment 1007} 1008 1009func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1010 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1011} 1012 1013type ReplyIssueCommentPlaceholderParams struct { 1014 LoggedInUser *oauth.MultiAccountUser 1015 RepoInfo repoinfo.RepoInfo 1016 Issue *models.Issue 1017 Comment *models.IssueComment 1018} 1019 1020func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1021 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1022} 1023 1024type ReplyIssueCommentParams struct { 1025 LoggedInUser *oauth.MultiAccountUser 1026 RepoInfo repoinfo.RepoInfo 1027 Issue *models.Issue 1028 Comment *models.IssueComment 1029} 1030 1031func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1032 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1033} 1034 1035type IssueCommentBodyParams struct { 1036 LoggedInUser *oauth.MultiAccountUser 1037 RepoInfo repoinfo.RepoInfo 1038 Issue *models.Issue 1039 Comment *models.IssueComment 1040} 1041 1042func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1043 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1044} 1045 1046type RepoNewPullParams struct { 1047 LoggedInUser *oauth.MultiAccountUser 1048 RepoInfo repoinfo.RepoInfo 1049 Branches []types.Branch 1050 Strategy string 1051 SourceBranch string 1052 TargetBranch string 1053 Title string 1054 Body string 1055 Active string 1056} 1057 1058func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1059 params.Active = "pulls" 1060 return p.executeRepo("repo/pulls/new", w, params) 1061} 1062 1063type RepoPullsParams struct { 1064 LoggedInUser *oauth.MultiAccountUser 1065 RepoInfo repoinfo.RepoInfo 1066 Pulls []*models.Pull 1067 Active string 1068 FilteringBy models.PullState 1069 FilterQuery string 1070 Stacks map[string]models.Stack 1071 Pipelines map[string]models.Pipeline 1072 LabelDefs map[string]*models.LabelDefinition 1073 Page pagination.Page 1074 PullCount int 1075} 1076 1077func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1078 params.Active = "pulls" 1079 return p.executeRepo("repo/pulls/pulls", w, params) 1080} 1081 1082type ResubmitResult uint64 1083 1084const ( 1085 ShouldResubmit ResubmitResult = iota 1086 ShouldNotResubmit 1087 Unknown 1088) 1089 1090func (r ResubmitResult) Yes() bool { 1091 return r == ShouldResubmit 1092} 1093func (r ResubmitResult) No() bool { 1094 return r == ShouldNotResubmit 1095} 1096func (r ResubmitResult) Unknown() bool { 1097 return r == Unknown 1098} 1099 1100type RepoSinglePullParams struct { 1101 LoggedInUser *oauth.MultiAccountUser 1102 RepoInfo repoinfo.RepoInfo 1103 Active string 1104 Pull *models.Pull 1105 Stack models.Stack 1106 AbandonedPulls []*models.Pull 1107 Backlinks []models.RichReferenceLink 1108 BranchDeleteStatus *models.BranchDeleteStatus 1109 MergeCheck types.MergeCheckResponse 1110 ResubmitCheck ResubmitResult 1111 Pipelines map[string]models.Pipeline 1112 Diff types.DiffRenderer 1113 DiffOpts types.DiffOpts 1114 ActiveRound int 1115 IsInterdiff bool 1116 1117 Reactions map[models.ReactionKind]models.ReactionDisplayData 1118 UserReacted map[models.ReactionKind]bool 1119 1120 LabelDefs map[string]*models.LabelDefinition 1121} 1122 1123func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1124 params.Active = "pulls" 1125 return p.executeRepo("repo/pulls/pull", w, params) 1126} 1127 1128type RepoPullPatchParams struct { 1129 LoggedInUser *oauth.MultiAccountUser 1130 RepoInfo repoinfo.RepoInfo 1131 Pull *models.Pull 1132 Stack models.Stack 1133 Diff *types.NiceDiff 1134 Round int 1135 Submission *models.PullSubmission 1136 DiffOpts types.DiffOpts 1137} 1138 1139// this name is a mouthful 1140func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1141 return p.execute("repo/pulls/patch", w, params) 1142} 1143 1144type RepoPullInterdiffParams struct { 1145 LoggedInUser *oauth.MultiAccountUser 1146 RepoInfo repoinfo.RepoInfo 1147 Pull *models.Pull 1148 Round int 1149 Interdiff *patchutil.InterdiffResult 1150 DiffOpts types.DiffOpts 1151} 1152 1153// this name is a mouthful 1154func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1155 return p.execute("repo/pulls/interdiff", w, params) 1156} 1157 1158type PullPatchUploadParams struct { 1159 RepoInfo repoinfo.RepoInfo 1160} 1161 1162func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1163 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1164} 1165 1166type PullCompareBranchesParams struct { 1167 RepoInfo repoinfo.RepoInfo 1168 Branches []types.Branch 1169 SourceBranch string 1170} 1171 1172func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1173 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1174} 1175 1176type PullCompareForkParams struct { 1177 RepoInfo repoinfo.RepoInfo 1178 Forks []models.Repo 1179 Selected string 1180} 1181 1182func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1183 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1184} 1185 1186type PullCompareForkBranchesParams struct { 1187 RepoInfo repoinfo.RepoInfo 1188 SourceBranches []types.Branch 1189 TargetBranches []types.Branch 1190} 1191 1192func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1193 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1194} 1195 1196type PullResubmitParams struct { 1197 LoggedInUser *oauth.MultiAccountUser 1198 RepoInfo repoinfo.RepoInfo 1199 Pull *models.Pull 1200 SubmissionId int 1201} 1202 1203func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1204 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1205} 1206 1207type PullActionsParams struct { 1208 LoggedInUser *oauth.MultiAccountUser 1209 RepoInfo repoinfo.RepoInfo 1210 Pull *models.Pull 1211 RoundNumber int 1212 MergeCheck types.MergeCheckResponse 1213 ResubmitCheck ResubmitResult 1214 BranchDeleteStatus *models.BranchDeleteStatus 1215 Stack models.Stack 1216} 1217 1218func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1219 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1220} 1221 1222type PullNewCommentParams struct { 1223 LoggedInUser *oauth.MultiAccountUser 1224 RepoInfo repoinfo.RepoInfo 1225 Pull *models.Pull 1226 RoundNumber int 1227} 1228 1229func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1230 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1231} 1232 1233type RepoCompareParams struct { 1234 LoggedInUser *oauth.MultiAccountUser 1235 RepoInfo repoinfo.RepoInfo 1236 Forks []models.Repo 1237 Branches []types.Branch 1238 Tags []*types.TagReference 1239 Base string 1240 Head string 1241 Diff *types.NiceDiff 1242 DiffOpts types.DiffOpts 1243 1244 Active string 1245} 1246 1247func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1248 params.Active = "overview" 1249 return p.executeRepo("repo/compare/compare", w, params) 1250} 1251 1252type RepoCompareNewParams struct { 1253 LoggedInUser *oauth.MultiAccountUser 1254 RepoInfo repoinfo.RepoInfo 1255 Forks []models.Repo 1256 Branches []types.Branch 1257 Tags []*types.TagReference 1258 Base string 1259 Head string 1260 1261 Active string 1262} 1263 1264func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1265 params.Active = "overview" 1266 return p.executeRepo("repo/compare/new", w, params) 1267} 1268 1269type RepoCompareAllowPullParams struct { 1270 LoggedInUser *oauth.MultiAccountUser 1271 RepoInfo repoinfo.RepoInfo 1272 Base string 1273 Head string 1274} 1275 1276func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1277 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1278} 1279 1280type RepoCompareDiffFragmentParams struct { 1281 Diff types.NiceDiff 1282 DiffOpts types.DiffOpts 1283} 1284 1285func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1286 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1287} 1288 1289type LabelPanelParams struct { 1290 LoggedInUser *oauth.MultiAccountUser 1291 RepoInfo repoinfo.RepoInfo 1292 Defs map[string]*models.LabelDefinition 1293 Subject string 1294 State models.LabelState 1295} 1296 1297func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1298 return p.executePlain("repo/fragments/labelPanel", w, params) 1299} 1300 1301type EditLabelPanelParams struct { 1302 LoggedInUser *oauth.MultiAccountUser 1303 RepoInfo repoinfo.RepoInfo 1304 Defs map[string]*models.LabelDefinition 1305 Subject string 1306 State models.LabelState 1307} 1308 1309func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1310 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1311} 1312 1313type PipelinesParams struct { 1314 LoggedInUser *oauth.MultiAccountUser 1315 RepoInfo repoinfo.RepoInfo 1316 Pipelines []models.Pipeline 1317 Active string 1318} 1319 1320func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1321 params.Active = "pipelines" 1322 return p.executeRepo("repo/pipelines/pipelines", w, params) 1323} 1324 1325type LogBlockParams struct { 1326 Id int 1327 Name string 1328 Command string 1329 Collapsed bool 1330 StartTime time.Time 1331} 1332 1333func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1334 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1335} 1336 1337type LogBlockEndParams struct { 1338 Id int 1339 StartTime time.Time 1340 EndTime time.Time 1341} 1342 1343func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1344 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1345} 1346 1347type LogLineParams struct { 1348 Id int 1349 Content string 1350} 1351 1352func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1353 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1354} 1355 1356type WorkflowParams struct { 1357 LoggedInUser *oauth.MultiAccountUser 1358 RepoInfo repoinfo.RepoInfo 1359 Pipeline models.Pipeline 1360 Workflow string 1361 LogUrl string 1362 Active string 1363} 1364 1365func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1366 params.Active = "pipelines" 1367 return p.executeRepo("repo/pipelines/workflow", w, params) 1368} 1369 1370type PutStringParams struct { 1371 LoggedInUser *oauth.MultiAccountUser 1372 Action string 1373 1374 // this is supplied in the case of editing an existing string 1375 String models.String 1376} 1377 1378func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1379 return p.execute("strings/put", w, params) 1380} 1381 1382type StringsDashboardParams struct { 1383 LoggedInUser *oauth.MultiAccountUser 1384 Card ProfileCard 1385 Strings []models.String 1386} 1387 1388func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1389 return p.execute("strings/dashboard", w, params) 1390} 1391 1392type StringTimelineParams struct { 1393 LoggedInUser *oauth.MultiAccountUser 1394 Strings []models.String 1395} 1396 1397func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1398 return p.execute("strings/timeline", w, params) 1399} 1400 1401type SingleStringParams struct { 1402 LoggedInUser *oauth.MultiAccountUser 1403 ShowRendered bool 1404 RenderToggle bool 1405 RenderedContents template.HTML 1406 String *models.String 1407 Stats models.StringStats 1408 IsStarred bool 1409 StarCount int 1410 Owner identity.Identity 1411} 1412 1413func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1414 return p.execute("strings/string", w, params) 1415} 1416 1417func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1418 return p.execute("timeline/home", w, params) 1419} 1420 1421func (p *Pages) Static() http.Handler { 1422 if p.dev { 1423 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1424 } 1425 1426 sub, err := fs.Sub(p.embedFS, "static") 1427 if err != nil { 1428 p.logger.Error("no static dir found? that's crazy", "err", err) 1429 panic(err) 1430 } 1431 // Custom handler to apply Cache-Control headers for font files 1432 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1433} 1434 1435func Cache(h http.Handler) http.Handler { 1436 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1437 path := strings.Split(r.URL.Path, "?")[0] 1438 1439 if strings.HasSuffix(path, ".css") { 1440 // on day for css files 1441 w.Header().Set("Cache-Control", "public, max-age=86400") 1442 } else { 1443 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1444 } 1445 h.ServeHTTP(w, r) 1446 }) 1447} 1448 1449func (p *Pages) CssContentHash() string { 1450 cssFile, err := p.embedFS.Open("static/tw.css") 1451 if err != nil { 1452 slog.Debug("Error opening CSS file", "err", err) 1453 return "" 1454 } 1455 defer cssFile.Close() 1456 1457 hasher := sha256.New() 1458 if _, err := io.Copy(hasher, cssFile); err != nil { 1459 slog.Debug("Error hashing CSS file", "err", err) 1460 return "" 1461 } 1462 1463 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1464} 1465 1466func (p *Pages) Error500(w io.Writer) error { 1467 return p.execute("errors/500", w, nil) 1468} 1469 1470func (p *Pages) Error404(w io.Writer) error { 1471 return p.execute("errors/404", w, nil) 1472} 1473 1474func (p *Pages) ErrorKnot404(w io.Writer) error { 1475 return p.execute("errors/knot404", w, nil) 1476} 1477 1478func (p *Pages) Error503(w io.Writer) error { 1479 return p.execute("errors/503", w, nil) 1480}