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