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