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