Monorepo for Tangled
at c94a9d40234c9f949fa38dd0bab2b6b0a40f34a6 1517 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 FollowStatus models.FollowStatus 527 Punchcard *models.Punchcard 528 Profile *models.Profile 529 Stats ProfileStats 530 Active string 531} 532 533type ProfileStats struct { 534 RepoCount int64 535 StarredCount int64 536 StringCount int64 537 FollowersCount int64 538 FollowingCount int64 539} 540 541func (p *ProfileCard) GetTabs() [][]any { 542 tabs := [][]any{ 543 {"overview", "overview", "square-chart-gantt", nil}, 544 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 545 {"starred", "starred", "star", p.Stats.StarredCount}, 546 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 547 } 548 549 return tabs 550} 551 552type ProfileOverviewParams struct { 553 LoggedInUser *oauth.MultiAccountUser 554 Repos []models.Repo 555 CollaboratingRepos []models.Repo 556 ProfileTimeline *models.ProfileTimeline 557 Card *ProfileCard 558 Active string 559} 560 561func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 562 params.Active = "overview" 563 return p.executeProfile("user/overview", w, params) 564} 565 566type ProfileReposParams struct { 567 LoggedInUser *oauth.MultiAccountUser 568 Repos []models.Repo 569 Card *ProfileCard 570 Active string 571} 572 573func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 574 params.Active = "repos" 575 return p.executeProfile("user/repos", w, params) 576} 577 578type ProfileStarredParams struct { 579 LoggedInUser *oauth.MultiAccountUser 580 Repos []models.Repo 581 Card *ProfileCard 582 Active string 583} 584 585func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 586 params.Active = "starred" 587 return p.executeProfile("user/starred", w, params) 588} 589 590type ProfileStringsParams struct { 591 LoggedInUser *oauth.MultiAccountUser 592 Strings []models.String 593 Card *ProfileCard 594 Active string 595} 596 597func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 598 params.Active = "strings" 599 return p.executeProfile("user/strings", w, params) 600} 601 602type FollowCard struct { 603 UserDid string 604 LoggedInUser *oauth.MultiAccountUser 605 FollowStatus models.FollowStatus 606 FollowersCount int64 607 FollowingCount int64 608 Profile *models.Profile 609} 610 611type ProfileFollowersParams struct { 612 LoggedInUser *oauth.MultiAccountUser 613 Followers []FollowCard 614 Card *ProfileCard 615 Active string 616} 617 618func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 619 params.Active = "overview" 620 return p.executeProfile("user/followers", w, params) 621} 622 623type ProfileFollowingParams struct { 624 LoggedInUser *oauth.MultiAccountUser 625 Following []FollowCard 626 Card *ProfileCard 627 Active string 628} 629 630func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 631 params.Active = "overview" 632 return p.executeProfile("user/following", w, params) 633} 634 635type FollowFragmentParams struct { 636 UserDid string 637 FollowStatus models.FollowStatus 638 FollowersCount int64 639} 640 641func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 642 return p.executePlain("user/fragments/follow-oob", w, params) 643} 644 645type EditBioParams struct { 646 LoggedInUser *oauth.MultiAccountUser 647 Profile *models.Profile 648} 649 650func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 651 return p.executePlain("user/fragments/editBio", w, params) 652} 653 654type EditPinsParams struct { 655 LoggedInUser *oauth.MultiAccountUser 656 Profile *models.Profile 657 AllRepos []PinnedRepo 658} 659 660type PinnedRepo struct { 661 IsPinned bool 662 models.Repo 663} 664 665func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 666 return p.executePlain("user/fragments/editPins", w, params) 667} 668 669type StarBtnFragmentParams struct { 670 IsStarred bool 671 SubjectAt syntax.ATURI 672 StarCount int 673 HxSwapOob bool 674} 675 676func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 677 params.HxSwapOob = true 678 return p.executePlain("fragments/starBtn", w, params) 679} 680 681type RepoIndexParams struct { 682 LoggedInUser *oauth.MultiAccountUser 683 RepoInfo repoinfo.RepoInfo 684 Active string 685 TagMap map[string][]string 686 CommitsTrunc []types.Commit 687 TagsTrunc []*types.TagReference 688 BranchesTrunc []types.Branch 689 // ForkInfo *types.ForkInfo 690 HTMLReadme template.HTML 691 Raw bool 692 EmailToDid map[string]string 693 VerifiedCommits commitverify.VerifiedCommits 694 Languages []types.RepoLanguageDetails 695 Pipelines map[string]models.Pipeline 696 NeedsKnotUpgrade bool 697 types.RepoIndexResponse 698} 699 700func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 701 params.Active = "overview" 702 if params.IsEmpty { 703 return p.executeRepo("repo/empty", w, params) 704 } 705 706 if params.NeedsKnotUpgrade { 707 return p.executeRepo("repo/needsUpgrade", w, params) 708 } 709 710 p.rctx.RepoInfo = params.RepoInfo 711 p.rctx.RepoInfo.Ref = params.Ref 712 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 713 714 if params.ReadmeFileName != "" { 715 ext := filepath.Ext(params.ReadmeFileName) 716 switch ext { 717 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 718 params.Raw = false 719 htmlString := p.rctx.RenderMarkdown(params.Readme) 720 sanitized := p.rctx.SanitizeDefault(htmlString) 721 params.HTMLReadme = template.HTML(sanitized) 722 default: 723 params.Raw = true 724 } 725 } 726 727 return p.executeRepo("repo/index", w, params) 728} 729 730type RepoLogParams struct { 731 LoggedInUser *oauth.MultiAccountUser 732 RepoInfo repoinfo.RepoInfo 733 TagMap map[string][]string 734 Active string 735 EmailToDid map[string]string 736 VerifiedCommits commitverify.VerifiedCommits 737 Pipelines map[string]models.Pipeline 738 739 types.RepoLogResponse 740} 741 742func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 743 params.Active = "overview" 744 return p.executeRepo("repo/log", w, params) 745} 746 747type RepoCommitParams struct { 748 LoggedInUser *oauth.MultiAccountUser 749 RepoInfo repoinfo.RepoInfo 750 Active string 751 EmailToDid map[string]string 752 Pipeline *models.Pipeline 753 DiffOpts types.DiffOpts 754 755 // singular because it's always going to be just one 756 VerifiedCommit commitverify.VerifiedCommits 757 758 types.RepoCommitResponse 759} 760 761func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 762 params.Active = "overview" 763 return p.executeRepo("repo/commit", w, params) 764} 765 766type RepoTreeParams struct { 767 LoggedInUser *oauth.MultiAccountUser 768 RepoInfo repoinfo.RepoInfo 769 Active string 770 BreadCrumbs [][]string 771 TreePath string 772 Raw bool 773 HTMLReadme template.HTML 774 EmailToDid map[string]string 775 LastCommitInfo *types.LastCommitInfo 776 types.RepoTreeResponse 777} 778 779type RepoTreeStats struct { 780 NumFolders uint64 781 NumFiles uint64 782} 783 784func (r RepoTreeParams) TreeStats() RepoTreeStats { 785 numFolders, numFiles := 0, 0 786 for _, f := range r.Files { 787 if !f.IsFile() { 788 numFolders += 1 789 } else if f.IsFile() { 790 numFiles += 1 791 } 792 } 793 794 return RepoTreeStats{ 795 NumFolders: uint64(numFolders), 796 NumFiles: uint64(numFiles), 797 } 798} 799 800func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 801 params.Active = "overview" 802 803 p.rctx.RepoInfo = params.RepoInfo 804 p.rctx.RepoInfo.Ref = params.Ref 805 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 806 807 if params.ReadmeFileName != "" { 808 ext := filepath.Ext(params.ReadmeFileName) 809 switch ext { 810 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 811 params.Raw = false 812 htmlString := p.rctx.RenderMarkdown(params.Readme) 813 sanitized := p.rctx.SanitizeDefault(htmlString) 814 params.HTMLReadme = template.HTML(sanitized) 815 default: 816 params.Raw = true 817 } 818 } 819 820 return p.executeRepo("repo/tree", w, params) 821} 822 823type RepoBranchesParams struct { 824 LoggedInUser *oauth.MultiAccountUser 825 RepoInfo repoinfo.RepoInfo 826 Active string 827 types.RepoBranchesResponse 828} 829 830func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 831 params.Active = "overview" 832 return p.executeRepo("repo/branches", w, params) 833} 834 835type RepoTagsParams struct { 836 LoggedInUser *oauth.MultiAccountUser 837 RepoInfo repoinfo.RepoInfo 838 Active string 839 types.RepoTagsResponse 840 ArtifactMap map[plumbing.Hash][]models.Artifact 841 DanglingArtifacts []models.Artifact 842} 843 844func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 845 params.Active = "overview" 846 return p.executeRepo("repo/tags", w, params) 847} 848 849type RepoTagParams struct { 850 LoggedInUser *oauth.MultiAccountUser 851 RepoInfo repoinfo.RepoInfo 852 Active string 853 types.RepoTagResponse 854 ArtifactMap map[plumbing.Hash][]models.Artifact 855 DanglingArtifacts []models.Artifact 856} 857 858func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 859 params.Active = "overview" 860 return p.executeRepo("repo/tag", w, params) 861} 862 863type RepoArtifactParams struct { 864 LoggedInUser *oauth.MultiAccountUser 865 RepoInfo repoinfo.RepoInfo 866 Artifact models.Artifact 867} 868 869func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 870 return p.executePlain("repo/fragments/artifact", w, params) 871} 872 873type RepoBlobParams struct { 874 LoggedInUser *oauth.MultiAccountUser 875 RepoInfo repoinfo.RepoInfo 876 Active string 877 BreadCrumbs [][]string 878 BlobView models.BlobView 879 EmailToDid map[string]string 880 LastCommitInfo *types.LastCommitInfo 881 *tangled.RepoBlob_Output 882} 883 884func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 885 switch params.BlobView.ContentType { 886 case models.BlobContentTypeMarkup: 887 p.rctx.RepoInfo = params.RepoInfo 888 } 889 890 params.Active = "overview" 891 return p.executeRepo("repo/blob", w, params) 892} 893 894type Collaborator struct { 895 Did string 896 Role string 897} 898 899type RepoSettingsParams struct { 900 LoggedInUser *oauth.MultiAccountUser 901 RepoInfo repoinfo.RepoInfo 902 Collaborators []Collaborator 903 Active string 904 Branches []types.Branch 905 Spindles []string 906 CurrentSpindle string 907 Secrets []*tangled.RepoListSecrets_Secret 908 909 // TODO: use repoinfo.roles 910 IsCollaboratorInviteAllowed bool 911} 912 913func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 914 params.Active = "settings" 915 return p.executeRepo("repo/settings", w, params) 916} 917 918type RepoGeneralSettingsParams struct { 919 LoggedInUser *oauth.MultiAccountUser 920 RepoInfo repoinfo.RepoInfo 921 Labels []models.LabelDefinition 922 DefaultLabels []models.LabelDefinition 923 SubscribedLabels map[string]struct{} 924 ShouldSubscribeAll bool 925 Active string 926 Tab string 927 Branches []types.Branch 928} 929 930func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 931 params.Active = "settings" 932 params.Tab = "general" 933 return p.executeRepo("repo/settings/general", w, params) 934} 935 936type RepoAccessSettingsParams struct { 937 LoggedInUser *oauth.MultiAccountUser 938 RepoInfo repoinfo.RepoInfo 939 Active string 940 Tab string 941 Collaborators []Collaborator 942} 943 944func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 945 params.Active = "settings" 946 params.Tab = "access" 947 return p.executeRepo("repo/settings/access", w, params) 948} 949 950type RepoPipelineSettingsParams struct { 951 LoggedInUser *oauth.MultiAccountUser 952 RepoInfo repoinfo.RepoInfo 953 Active string 954 Tab string 955 Spindles []string 956 CurrentSpindle string 957 Secrets []map[string]any 958} 959 960func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 961 params.Active = "settings" 962 params.Tab = "pipelines" 963 return p.executeRepo("repo/settings/pipelines", w, params) 964} 965 966type RepoIssuesParams struct { 967 LoggedInUser *oauth.MultiAccountUser 968 RepoInfo repoinfo.RepoInfo 969 Active string 970 Issues []models.Issue 971 IssueCount int 972 LabelDefs map[string]*models.LabelDefinition 973 Page pagination.Page 974 FilteringByOpen bool 975 FilterQuery string 976} 977 978func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 979 params.Active = "issues" 980 return p.executeRepo("repo/issues/issues", w, params) 981} 982 983type RepoSingleIssueParams struct { 984 LoggedInUser *oauth.MultiAccountUser 985 RepoInfo repoinfo.RepoInfo 986 Active string 987 Issue *models.Issue 988 CommentList []models.CommentListItem 989 Backlinks []models.RichReferenceLink 990 LabelDefs map[string]*models.LabelDefinition 991 992 Reactions map[models.ReactionKind]models.ReactionDisplayData 993 UserReacted map[models.ReactionKind]bool 994} 995 996func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 997 params.Active = "issues" 998 return p.executeRepo("repo/issues/issue", w, params) 999} 1000 1001type EditIssueParams struct { 1002 LoggedInUser *oauth.MultiAccountUser 1003 RepoInfo repoinfo.RepoInfo 1004 Issue *models.Issue 1005 Action string 1006} 1007 1008func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1009 params.Action = "edit" 1010 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1011} 1012 1013type ThreadReactionFragmentParams struct { 1014 ThreadAt syntax.ATURI 1015 Kind models.ReactionKind 1016 Count int 1017 Users []string 1018 IsReacted bool 1019} 1020 1021func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1022 return p.executePlain("repo/fragments/reaction", w, params) 1023} 1024 1025type RepoNewIssueParams struct { 1026 LoggedInUser *oauth.MultiAccountUser 1027 RepoInfo repoinfo.RepoInfo 1028 Issue *models.Issue // existing issue if any -- passed when editing 1029 Active string 1030 Action string 1031} 1032 1033func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1034 params.Active = "issues" 1035 params.Action = "create" 1036 return p.executeRepo("repo/issues/new", w, params) 1037} 1038 1039type EditIssueCommentParams struct { 1040 LoggedInUser *oauth.MultiAccountUser 1041 RepoInfo repoinfo.RepoInfo 1042 Issue *models.Issue 1043 Comment *models.IssueComment 1044} 1045 1046func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 1047 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 1048} 1049 1050type ReplyIssueCommentPlaceholderParams struct { 1051 LoggedInUser *oauth.MultiAccountUser 1052 RepoInfo repoinfo.RepoInfo 1053 Issue *models.Issue 1054 Comment *models.IssueComment 1055} 1056 1057func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1058 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1059} 1060 1061type ReplyIssueCommentParams struct { 1062 LoggedInUser *oauth.MultiAccountUser 1063 RepoInfo repoinfo.RepoInfo 1064 Issue *models.Issue 1065 Comment *models.IssueComment 1066} 1067 1068func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1069 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1070} 1071 1072type IssueCommentBodyParams struct { 1073 LoggedInUser *oauth.MultiAccountUser 1074 RepoInfo repoinfo.RepoInfo 1075 Issue *models.Issue 1076 Comment *models.IssueComment 1077} 1078 1079func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1080 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1081} 1082 1083type RepoNewPullParams struct { 1084 LoggedInUser *oauth.MultiAccountUser 1085 RepoInfo repoinfo.RepoInfo 1086 Branches []types.Branch 1087 Strategy string 1088 SourceBranch string 1089 TargetBranch string 1090 Title string 1091 Body string 1092 Active string 1093} 1094 1095func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1096 params.Active = "pulls" 1097 return p.executeRepo("repo/pulls/new", w, params) 1098} 1099 1100type RepoPullsParams struct { 1101 LoggedInUser *oauth.MultiAccountUser 1102 RepoInfo repoinfo.RepoInfo 1103 Pulls []*models.Pull 1104 Active string 1105 FilteringBy models.PullState 1106 FilterQuery string 1107 Stacks map[string]models.Stack 1108 Pipelines map[string]models.Pipeline 1109 LabelDefs map[string]*models.LabelDefinition 1110 Page pagination.Page 1111 PullCount int 1112} 1113 1114func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1115 params.Active = "pulls" 1116 return p.executeRepo("repo/pulls/pulls", w, params) 1117} 1118 1119type ResubmitResult uint64 1120 1121const ( 1122 ShouldResubmit ResubmitResult = iota 1123 ShouldNotResubmit 1124 Unknown 1125) 1126 1127func (r ResubmitResult) Yes() bool { 1128 return r == ShouldResubmit 1129} 1130func (r ResubmitResult) No() bool { 1131 return r == ShouldNotResubmit 1132} 1133func (r ResubmitResult) Unknown() bool { 1134 return r == Unknown 1135} 1136 1137type RepoSinglePullParams struct { 1138 LoggedInUser *oauth.MultiAccountUser 1139 RepoInfo repoinfo.RepoInfo 1140 Active string 1141 Pull *models.Pull 1142 Stack models.Stack 1143 AbandonedPulls []*models.Pull 1144 Backlinks []models.RichReferenceLink 1145 BranchDeleteStatus *models.BranchDeleteStatus 1146 MergeCheck types.MergeCheckResponse 1147 ResubmitCheck ResubmitResult 1148 Pipelines map[string]models.Pipeline 1149 Diff types.DiffRenderer 1150 DiffOpts types.DiffOpts 1151 ActiveRound int 1152 IsInterdiff bool 1153 1154 Reactions map[models.ReactionKind]models.ReactionDisplayData 1155 UserReacted map[models.ReactionKind]bool 1156 1157 LabelDefs map[string]*models.LabelDefinition 1158} 1159 1160func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1161 params.Active = "pulls" 1162 return p.executeRepo("repo/pulls/pull", w, params) 1163} 1164 1165type RepoPullPatchParams struct { 1166 LoggedInUser *oauth.MultiAccountUser 1167 RepoInfo repoinfo.RepoInfo 1168 Pull *models.Pull 1169 Stack models.Stack 1170 Diff *types.NiceDiff 1171 Round int 1172 Submission *models.PullSubmission 1173 DiffOpts types.DiffOpts 1174} 1175 1176// this name is a mouthful 1177func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1178 return p.execute("repo/pulls/patch", w, params) 1179} 1180 1181type RepoPullInterdiffParams struct { 1182 LoggedInUser *oauth.MultiAccountUser 1183 RepoInfo repoinfo.RepoInfo 1184 Pull *models.Pull 1185 Round int 1186 Interdiff *patchutil.InterdiffResult 1187 DiffOpts types.DiffOpts 1188} 1189 1190// this name is a mouthful 1191func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1192 return p.execute("repo/pulls/interdiff", w, params) 1193} 1194 1195type PullPatchUploadParams struct { 1196 RepoInfo repoinfo.RepoInfo 1197} 1198 1199func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1200 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1201} 1202 1203type PullCompareBranchesParams struct { 1204 RepoInfo repoinfo.RepoInfo 1205 Branches []types.Branch 1206 SourceBranch string 1207} 1208 1209func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1210 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1211} 1212 1213type PullCompareForkParams struct { 1214 RepoInfo repoinfo.RepoInfo 1215 Forks []models.Repo 1216 Selected string 1217} 1218 1219func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1220 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1221} 1222 1223type PullCompareForkBranchesParams struct { 1224 RepoInfo repoinfo.RepoInfo 1225 SourceBranches []types.Branch 1226 TargetBranches []types.Branch 1227} 1228 1229func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1230 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1231} 1232 1233type PullResubmitParams struct { 1234 LoggedInUser *oauth.MultiAccountUser 1235 RepoInfo repoinfo.RepoInfo 1236 Pull *models.Pull 1237 SubmissionId int 1238} 1239 1240func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1241 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1242} 1243 1244type PullActionsParams struct { 1245 LoggedInUser *oauth.MultiAccountUser 1246 RepoInfo repoinfo.RepoInfo 1247 Pull *models.Pull 1248 RoundNumber int 1249 MergeCheck types.MergeCheckResponse 1250 ResubmitCheck ResubmitResult 1251 BranchDeleteStatus *models.BranchDeleteStatus 1252 Stack models.Stack 1253} 1254 1255func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1256 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1257} 1258 1259type PullNewCommentParams struct { 1260 LoggedInUser *oauth.MultiAccountUser 1261 RepoInfo repoinfo.RepoInfo 1262 Pull *models.Pull 1263 RoundNumber int 1264} 1265 1266func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1267 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1268} 1269 1270type RepoCompareParams struct { 1271 LoggedInUser *oauth.MultiAccountUser 1272 RepoInfo repoinfo.RepoInfo 1273 Forks []models.Repo 1274 Branches []types.Branch 1275 Tags []*types.TagReference 1276 Base string 1277 Head string 1278 Diff *types.NiceDiff 1279 DiffOpts types.DiffOpts 1280 1281 Active string 1282} 1283 1284func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1285 params.Active = "overview" 1286 return p.executeRepo("repo/compare/compare", w, params) 1287} 1288 1289type RepoCompareNewParams struct { 1290 LoggedInUser *oauth.MultiAccountUser 1291 RepoInfo repoinfo.RepoInfo 1292 Forks []models.Repo 1293 Branches []types.Branch 1294 Tags []*types.TagReference 1295 Base string 1296 Head string 1297 1298 Active string 1299} 1300 1301func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1302 params.Active = "overview" 1303 return p.executeRepo("repo/compare/new", w, params) 1304} 1305 1306type RepoCompareAllowPullParams struct { 1307 LoggedInUser *oauth.MultiAccountUser 1308 RepoInfo repoinfo.RepoInfo 1309 Base string 1310 Head string 1311} 1312 1313func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1314 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1315} 1316 1317type RepoCompareDiffFragmentParams struct { 1318 Diff types.NiceDiff 1319 DiffOpts types.DiffOpts 1320} 1321 1322func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1323 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1324} 1325 1326type LabelPanelParams struct { 1327 LoggedInUser *oauth.MultiAccountUser 1328 RepoInfo repoinfo.RepoInfo 1329 Defs map[string]*models.LabelDefinition 1330 Subject string 1331 State models.LabelState 1332} 1333 1334func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1335 return p.executePlain("repo/fragments/labelPanel", w, params) 1336} 1337 1338type EditLabelPanelParams struct { 1339 LoggedInUser *oauth.MultiAccountUser 1340 RepoInfo repoinfo.RepoInfo 1341 Defs map[string]*models.LabelDefinition 1342 Subject string 1343 State models.LabelState 1344} 1345 1346func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1347 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1348} 1349 1350type PipelinesParams struct { 1351 LoggedInUser *oauth.MultiAccountUser 1352 RepoInfo repoinfo.RepoInfo 1353 Pipelines []models.Pipeline 1354 Active string 1355} 1356 1357func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1358 params.Active = "pipelines" 1359 return p.executeRepo("repo/pipelines/pipelines", w, params) 1360} 1361 1362type LogBlockParams struct { 1363 Id int 1364 Name string 1365 Command string 1366 Collapsed bool 1367 StartTime time.Time 1368} 1369 1370func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1371 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1372} 1373 1374type LogBlockEndParams struct { 1375 Id int 1376 StartTime time.Time 1377 EndTime time.Time 1378} 1379 1380func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1381 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1382} 1383 1384type LogLineParams struct { 1385 Id int 1386 Content string 1387} 1388 1389func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1390 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1391} 1392 1393type WorkflowParams struct { 1394 LoggedInUser *oauth.MultiAccountUser 1395 RepoInfo repoinfo.RepoInfo 1396 Pipeline models.Pipeline 1397 Workflow string 1398 LogUrl string 1399 Active string 1400} 1401 1402func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1403 params.Active = "pipelines" 1404 return p.executeRepo("repo/pipelines/workflow", w, params) 1405} 1406 1407type PutStringParams struct { 1408 LoggedInUser *oauth.MultiAccountUser 1409 Action string 1410 1411 // this is supplied in the case of editing an existing string 1412 String models.String 1413} 1414 1415func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1416 return p.execute("strings/put", w, params) 1417} 1418 1419type StringsDashboardParams struct { 1420 LoggedInUser *oauth.MultiAccountUser 1421 Card ProfileCard 1422 Strings []models.String 1423} 1424 1425func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1426 return p.execute("strings/dashboard", w, params) 1427} 1428 1429type StringTimelineParams struct { 1430 LoggedInUser *oauth.MultiAccountUser 1431 Strings []models.String 1432} 1433 1434func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1435 return p.execute("strings/timeline", w, params) 1436} 1437 1438type SingleStringParams struct { 1439 LoggedInUser *oauth.MultiAccountUser 1440 ShowRendered bool 1441 RenderToggle bool 1442 RenderedContents template.HTML 1443 String *models.String 1444 Stats models.StringStats 1445 IsStarred bool 1446 StarCount int 1447 Owner identity.Identity 1448} 1449 1450func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1451 return p.execute("strings/string", w, params) 1452} 1453 1454func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1455 return p.execute("timeline/home", w, params) 1456} 1457 1458func (p *Pages) Static() http.Handler { 1459 if p.dev { 1460 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1461 } 1462 1463 sub, err := fs.Sub(p.embedFS, "static") 1464 if err != nil { 1465 p.logger.Error("no static dir found? that's crazy", "err", err) 1466 panic(err) 1467 } 1468 // Custom handler to apply Cache-Control headers for font files 1469 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1470} 1471 1472func Cache(h http.Handler) http.Handler { 1473 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1474 path := strings.Split(r.URL.Path, "?")[0] 1475 1476 if strings.HasSuffix(path, ".css") { 1477 // on day for css files 1478 w.Header().Set("Cache-Control", "public, max-age=86400") 1479 } else { 1480 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1481 } 1482 h.ServeHTTP(w, r) 1483 }) 1484} 1485 1486func (p *Pages) CssContentHash() string { 1487 cssFile, err := p.embedFS.Open("static/tw.css") 1488 if err != nil { 1489 slog.Debug("Error opening CSS file", "err", err) 1490 return "" 1491 } 1492 defer cssFile.Close() 1493 1494 hasher := sha256.New() 1495 if _, err := io.Copy(hasher, cssFile); err != nil { 1496 slog.Debug("Error hashing CSS file", "err", err) 1497 return "" 1498 } 1499 1500 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1501} 1502 1503func (p *Pages) Error500(w io.Writer) error { 1504 return p.execute("errors/500", w, nil) 1505} 1506 1507func (p *Pages) Error404(w io.Writer) error { 1508 return p.execute("errors/404", w, nil) 1509} 1510 1511func (p *Pages) ErrorKnot404(w io.Writer) error { 1512 return p.execute("errors/knot404", w, nil) 1513} 1514 1515func (p *Pages) Error503(w io.Writer) error { 1516 return p.execute("errors/503", w, nil) 1517}