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