This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / pages.go
47 kB 1780 lines
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/cache" 21 "tangled.org/core/appview/commitverify" 22 "tangled.org/core/appview/config" 23 "tangled.org/core/appview/db" 24 "tangled.org/core/appview/models" 25 "tangled.org/core/appview/oauth" 26 "tangled.org/core/appview/pages/markup" 27 "tangled.org/core/appview/pages/repoinfo" 28 "tangled.org/core/appview/pagination" 29 "tangled.org/core/idresolver" 30 "tangled.org/core/patchutil" 31 "tangled.org/core/types" 32 33 "github.com/bluesky-social/indigo/atproto/identity" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 "github.com/go-git/go-git/v5/plumbing" 36) 37 38//go:embed templates/* static legal 39var Files embed.FS 40 41type Pages struct { 42 mu sync.RWMutex 43 cache *TmplCache[string, *template.Template] 44 45 avatar config.AvatarConfig 46 pdsCfg config.PdsConfig 47 resolver *idresolver.Resolver 48 db *db.DB 49 rdb *cache.Cache 50 dev bool 51 embedFS fs.FS 52 templateDir string // Path to templates on disk for dev mode 53 rctx *markup.RenderContext 54 logger *slog.Logger 55} 56 57func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 58 // initialized with safe defaults, can be overridden per use 59 rctx := &markup.RenderContext{ 60 IsDev: config.Core.Dev, 61 Hostname: config.Core.AppviewHost, 62 CamoUrl: config.Camo.Host, 63 CamoSecret: config.Camo.SharedSecret, 64 Sanitizer: markup.NewSanitizer(), 65 Files: Files, 66 } 67 68 p := &Pages{ 69 mu: sync.RWMutex{}, 70 cache: NewTmplCache[string, *template.Template](), 71 dev: config.Core.Dev, 72 avatar: config.Avatar, 73 pdsCfg: config.Pds, 74 rctx: rctx, 75 resolver: res, 76 db: database, 77 rdb: rdb, 78 templateDir: "appview/pages", 79 logger: logger, 80 } 81 82 if p.dev { 83 p.embedFS = os.DirFS(p.templateDir) 84 } else { 85 p.embedFS = Files 86 } 87 88 return p 89} 90 91// reverse of pathToName 92func (p *Pages) nameToPath(s string) string { 93 return "templates/" + s + ".html" 94} 95 96// FuncMap returns the template function map for use by external template consumers. 97func (p *Pages) FuncMap() template.FuncMap { 98 return p.funcMap() 99} 100 101// FragmentPaths returns all fragment template paths from the embedded FS. 102func (p *Pages) FragmentPaths() ([]string, error) { 103 return p.fragmentPaths() 104} 105 106// EmbedFS returns the embedded filesystem containing templates and static assets. 107func (p *Pages) EmbedFS() fs.FS { 108 return p.embedFS 109} 110 111// ParseWith parses the base layout together with all appview fragments and 112// an additional template from extraFS identified by extraPath (relative to 113// extraFS root). The returned template is ready to ExecuteTemplate with 114// "layouts/base" -- primarily for use with the blog. 115func (p *Pages) ParseWith(extraFS fs.FS, extraPath string) (*template.Template, error) { 116 fragmentPaths, err := p.fragmentPaths() 117 if err != nil { 118 return nil, err 119 } 120 121 funcs := p.funcMap() 122 tpl, err := template.New("layouts/base"). 123 Funcs(funcs). 124 ParseFS(p.embedFS, append(fragmentPaths, p.nameToPath("layouts/base"))...) 125 if err != nil { 126 return nil, err 127 } 128 129 err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { 130 if err != nil { 131 return err 132 } 133 if d.IsDir() || !strings.HasSuffix(path, ".html") { 134 return nil 135 } 136 if path != extraPath && !strings.Contains(path, "fragments/") { 137 return nil 138 } 139 data, err := fs.ReadFile(extraFS, path) 140 if err != nil { 141 return err 142 } 143 if _, err = tpl.New(path).Parse(string(data)); err != nil { 144 return err 145 } 146 return nil 147 }) 148 if err != nil { 149 return nil, err 150 } 151 152 return tpl, nil 153} 154 155func (p *Pages) fragmentPaths() ([]string, error) { 156 var fragmentPaths []string 157 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 158 if err != nil { 159 return err 160 } 161 if d.IsDir() { 162 return nil 163 } 164 if !strings.HasSuffix(path, ".html") { 165 return nil 166 } 167 if !strings.Contains(path, "fragments/") { 168 return nil 169 } 170 fragmentPaths = append(fragmentPaths, path) 171 return nil 172 }) 173 if err != nil { 174 return nil, err 175 } 176 177 return fragmentPaths, nil 178} 179 180// parse without memoization 181func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 182 paths, err := p.fragmentPaths() 183 if err != nil { 184 return nil, err 185 } 186 for _, s := range stack { 187 paths = append(paths, p.nameToPath(s)) 188 } 189 190 funcs := p.funcMap() 191 top := stack[len(stack)-1] 192 parsed, err := template.New(top). 193 Funcs(funcs). 194 ParseFS(p.embedFS, paths...) 195 if err != nil { 196 return nil, err 197 } 198 199 return parsed, nil 200} 201 202func (p *Pages) parse(stack ...string) (*template.Template, error) { 203 key := strings.Join(stack, "|") 204 205 // never cache in dev mode 206 if cached, exists := p.cache.Get(key); !p.dev && exists { 207 return cached, nil 208 } 209 210 result, err := p.rawParse(stack...) 211 if err != nil { 212 return nil, err 213 } 214 215 p.cache.Set(key, result) 216 return result, nil 217} 218 219func (p *Pages) parseBase(top string) (*template.Template, error) { 220 stack := []string{ 221 "layouts/base", 222 top, 223 } 224 return p.parse(stack...) 225} 226 227func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 228 stack := []string{ 229 "layouts/base", 230 "layouts/repobase", 231 top, 232 } 233 return p.parse(stack...) 234} 235 236func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 237 stack := []string{ 238 "layouts/base", 239 "layouts/profilebase", 240 top, 241 } 242 return p.parse(stack...) 243} 244 245func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 246 stack := []string{ 247 "layouts/base", 248 "layouts/loginbase", 249 top, 250 } 251 return p.parse(stack...) 252} 253 254func (p *Pages) executePlain(name string, w io.Writer, params any) error { 255 tpl, err := p.parse(name) 256 if err != nil { 257 return err 258 } 259 260 return tpl.Execute(w, params) 261} 262 263func (p *Pages) executeLogin(name string, w io.Writer, params any) error { 264 tpl, err := p.parseLoginBase(name) 265 if err != nil { 266 return err 267 } 268 269 return tpl.ExecuteTemplate(w, "layouts/base", params) 270} 271 272func (p *Pages) execute(name string, w io.Writer, params any) error { 273 tpl, err := p.parseBase(name) 274 if err != nil { 275 return err 276 } 277 278 return tpl.ExecuteTemplate(w, "layouts/base", params) 279} 280 281func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 282 tpl, err := p.parseRepoBase(name) 283 if err != nil { 284 return err 285 } 286 287 return tpl.ExecuteTemplate(w, "layouts/base", params) 288} 289 290func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 291 tpl, err := p.parseProfileBase(name) 292 if err != nil { 293 return err 294 } 295 296 return tpl.ExecuteTemplate(w, "layouts/base", params) 297} 298 299type DollyParams struct { 300 Classes string 301 FillColor string 302} 303 304func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 305 return p.executePlain("fragments/dolly/logo", w, params) 306} 307 308func (p *Pages) Favicon(w io.Writer) error { 309 return p.Dolly(w, DollyParams{ 310 Classes: "text-black dark:text-white", 311 }) 312} 313 314type LoginParams struct { 315 ReturnUrl string 316 ErrorCode string 317 AddAccount bool 318 Accounts []oauth.AccountInfo 319} 320 321func (p *Pages) Login(w io.Writer, params LoginParams) error { 322 return p.executeLogin("user/login", w, params) 323} 324 325type SignupParams struct { 326 CloudflareSiteKey string 327 EmailId string 328} 329 330func (p *Pages) Signup(w io.Writer, params SignupParams) error { 331 return p.executeLogin("user/signup", w, params) 332} 333 334func (p *Pages) CompleteSignup(w io.Writer) error { 335 return p.executeLogin("user/completeSignup", w, nil) 336} 337 338type TermsOfServiceParams struct { 339 LoggedInUser *oauth.MultiAccountUser 340 Content template.HTML 341} 342 343func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 344 filename := "terms.md" 345 filePath := filepath.Join("legal", filename) 346 347 file, err := p.embedFS.Open(filePath) 348 if err != nil { 349 return fmt.Errorf("failed to read %s: %w", filename, err) 350 } 351 defer file.Close() 352 353 markdownBytes, err := io.ReadAll(file) 354 if err != nil { 355 return fmt.Errorf("failed to read %s: %w", filename, err) 356 } 357 358 rctx := p.rctx.Clone() 359 rctx.RendererType = markup.RendererTypeDefault 360 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 361 sanitized := rctx.SanitizeDefault(htmlString) 362 params.Content = template.HTML(sanitized) 363 364 return p.execute("legal/terms", w, params) 365} 366 367type PrivacyPolicyParams struct { 368 LoggedInUser *oauth.MultiAccountUser 369 Content template.HTML 370} 371 372func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 373 filename := "privacy.md" 374 filePath := filepath.Join("legal", filename) 375 376 file, err := p.embedFS.Open(filePath) 377 if err != nil { 378 return fmt.Errorf("failed to read %s: %w", filename, err) 379 } 380 defer file.Close() 381 382 markdownBytes, err := io.ReadAll(file) 383 if err != nil { 384 return fmt.Errorf("failed to read %s: %w", filename, err) 385 } 386 387 rctx := p.rctx.Clone() 388 rctx.RendererType = markup.RendererTypeDefault 389 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 390 sanitized := rctx.SanitizeDefault(htmlString) 391 params.Content = template.HTML(sanitized) 392 393 return p.execute("legal/privacy", w, params) 394} 395 396type BrandParams struct { 397 LoggedInUser *oauth.MultiAccountUser 398} 399 400func (p *Pages) Brand(w io.Writer, params BrandParams) error { 401 return p.execute("brand/brand", w, params) 402} 403 404type TimelineParams struct { 405 LoggedInUser *oauth.MultiAccountUser 406 Timeline []models.TimelineGroup 407 Repos []models.Repo 408 GfiLabel *models.LabelDefinition 409 BlueskyPosts []models.BskyPost 410 VouchSuggestions []models.VouchSuggestion 411 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 412 // For logged-in users it reflects their newsletter_preferences row; for 413 // anonymous visitors it is always true (dismissal falls back to 414 // localStorage on the client). 415 ShowNewsletter bool 416} 417 418func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 419 return p.execute("timeline/timeline", w, params) 420} 421 422type GoodFirstIssuesParams struct { 423 LoggedInUser *oauth.MultiAccountUser 424 Issues []models.Issue 425 RepoGroups []*models.RepoGroup 426 LabelDefs map[string]*models.LabelDefinition 427 GfiLabel *models.LabelDefinition 428 Page pagination.Page 429} 430 431func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 432 return p.execute("goodfirstissues/index", w, params) 433} 434 435type UserProfileSettingsParams struct { 436 LoggedInUser *oauth.MultiAccountUser 437 Tab string 438 PunchcardPreference models.PunchcardPreference 439 IsTnglSh bool 440 IsDeactivated bool 441 HandleOpen bool 442} 443 444func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 445 params.Tab = "profile" 446 return p.execute("user/settings/profile", w, params) 447} 448 449type NotificationsParams struct { 450 LoggedInUser *oauth.MultiAccountUser 451 Notifications []*models.NotificationWithEntity 452 UnreadCount int 453 Page pagination.Page 454 Total int64 455} 456 457func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 458 return p.execute("notifications/list", w, params) 459} 460 461type NotificationItemParams struct { 462 Notification *models.Notification 463} 464 465func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 466 return p.executePlain("notifications/fragments/item", w, params) 467} 468 469type NotificationCountParams struct { 470 Count int64 471} 472 473func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 474 return p.executePlain("notifications/fragments/count", w, params) 475} 476 477type UserKeysSettingsParams struct { 478 LoggedInUser *oauth.MultiAccountUser 479 PubKeys []models.PublicKey 480 Tab string 481} 482 483func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 484 params.Tab = "keys" 485 return p.execute("user/settings/keys", w, params) 486} 487 488type UserEmailsSettingsParams struct { 489 LoggedInUser *oauth.MultiAccountUser 490 Emails []models.Email 491 Tab string 492} 493 494func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 495 params.Tab = "emails" 496 return p.execute("user/settings/emails", w, params) 497} 498 499type UserNotificationSettingsParams struct { 500 LoggedInUser *oauth.MultiAccountUser 501 Preferences *models.NotificationPreferences 502 Tab string 503} 504 505func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 506 params.Tab = "notifications" 507 return p.execute("user/settings/notifications", w, params) 508} 509 510type UserSiteSettingsParams struct { 511 LoggedInUser *oauth.MultiAccountUser 512 Claim *models.DomainClaim 513 SitesDomain string 514 IsTnglHandle bool 515 Tab string 516} 517 518func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 519 params.Tab = "sites" 520 return p.execute("user/settings/sites", w, params) 521} 522 523type UpgradeBannerParams struct { 524 Registrations []models.Registration 525 Spindles []models.Spindle 526} 527 528func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 529 return p.executePlain("banner", w, params) 530} 531 532type NewsletterResponseParams struct { 533 // Id identifies the calling form instance; the response span's id will 534 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 535 Id string 536 // Error, when non-empty, switches the template to the error variant. 537 Error string 538} 539 540func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 541 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 542} 543 544type KnotsParams struct { 545 LoggedInUser *oauth.MultiAccountUser 546 Registrations []models.Registration 547 Tab string 548} 549 550func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 551 params.Tab = "knots" 552 return p.execute("knots/index", w, params) 553} 554 555type KnotParams struct { 556 LoggedInUser *oauth.MultiAccountUser 557 Registration *models.Registration 558 Members []string 559 Repos map[string][]models.Repo 560 IsOwner bool 561 Tab string 562} 563 564func (p *Pages) Knot(w io.Writer, params KnotParams) error { 565 return p.execute("knots/dashboard", w, params) 566} 567 568type KnotListingParams struct { 569 *models.Registration 570} 571 572func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 573 return p.executePlain("knots/fragments/knotListing", w, params) 574} 575 576type SpindlesParams struct { 577 LoggedInUser *oauth.MultiAccountUser 578 Spindles []models.Spindle 579 Tab string 580} 581 582func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 583 params.Tab = "spindles" 584 return p.execute("spindles/index", w, params) 585} 586 587type SpindleListingParams struct { 588 models.Spindle 589 Tab string 590} 591 592func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 593 return p.executePlain("spindles/fragments/spindleListing", w, params) 594} 595 596type SpindleDashboardParams struct { 597 LoggedInUser *oauth.MultiAccountUser 598 Spindle models.Spindle 599 Members []string 600 Repos map[string][]models.Repo 601 Tab string 602} 603 604func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 605 return p.execute("spindles/dashboard", w, params) 606} 607 608type NewRepoParams struct { 609 LoggedInUser *oauth.MultiAccountUser 610 Knots []string 611} 612 613func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 614 return p.execute("repo/new", w, params) 615} 616 617type ForkRepoParams struct { 618 LoggedInUser *oauth.MultiAccountUser 619 Knots []string 620 RepoInfo repoinfo.RepoInfo 621} 622 623func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 624 return p.execute("repo/fork", w, params) 625} 626 627type ProfileCard struct { 628 UserDid string 629 HasProfile bool 630 FollowStatus models.FollowStatus 631 VouchRelationship *models.VouchRelationship 632 Punchcard *models.Punchcard 633 Profile *models.Profile 634 Stats ProfileStats 635 Active string 636} 637 638type ProfileStats struct { 639 RepoCount int64 640 StarredCount int64 641 StringCount int64 642 FollowersCount int64 643 FollowingCount int64 644} 645 646func (p *ProfileCard) GetTabs() [][]any { 647 tabs := [][]any{ 648 {"overview", "overview", "square-chart-gantt", nil}, 649 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 650 {"starred", "starred", "star", p.Stats.StarredCount}, 651 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 652 {"vouches", "vouches", "shield", nil}, 653 } 654 655 return tabs 656} 657 658type ProfileOverviewParams struct { 659 LoggedInUser *oauth.MultiAccountUser 660 Repos []models.Repo 661 CollaboratingRepos []models.Repo 662 ProfileTimeline *models.ProfileTimeline 663 Card *ProfileCard 664 Active string 665 ShowPunchcard bool 666} 667 668func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 669 params.Active = "overview" 670 return p.executeProfile("user/overview", w, params) 671} 672 673type ProfileReposParams struct { 674 LoggedInUser *oauth.MultiAccountUser 675 Repos []models.Repo 676 Card *ProfileCard 677 Active string 678 Page pagination.Page 679 RepoCount int 680 FilterQuery string 681} 682 683func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 684 params.Active = "repos" 685 return p.executeProfile("user/repos", w, params) 686} 687 688type ProfileStarredParams struct { 689 LoggedInUser *oauth.MultiAccountUser 690 Repos []models.Repo 691 Card *ProfileCard 692 Page pagination.Page 693 Total int 694 Active string 695} 696 697func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 698 params.Active = "starred" 699 return p.executeProfile("user/starred", w, params) 700} 701 702type ProfileStringsParams struct { 703 LoggedInUser *oauth.MultiAccountUser 704 Strings []models.String 705 Card *ProfileCard 706 Active string 707} 708 709func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 710 params.Active = "strings" 711 return p.executeProfile("user/strings", w, params) 712} 713 714type ProfileVouchesParams struct { 715 LoggedInUser *oauth.MultiAccountUser 716 Vouches []models.Vouch 717 Suggestions []models.VouchSuggestion 718 Card *ProfileCard 719 Page pagination.Page 720 VouchCount int 721 Active string 722 EvidencePulls map[syntax.ATURI]*models.Pull 723 EvidenceIssues map[syntax.ATURI]*models.Issue 724} 725 726func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 727 params.Active = "vouches" 728 return p.executeProfile("user/vouches", w, params) 729} 730 731type FollowCard struct { 732 UserDid string 733 LoggedInUser *oauth.MultiAccountUser 734 FollowStatus models.FollowStatus 735 FollowersCount int64 736 FollowingCount int64 737 Profile *models.Profile 738} 739 740type ProfileFollowersParams struct { 741 LoggedInUser *oauth.MultiAccountUser 742 Followers []FollowCard 743 Card *ProfileCard 744 Active string 745} 746 747func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 748 params.Active = "overview" 749 return p.executeProfile("user/followers", w, params) 750} 751 752type ProfileFollowingParams struct { 753 LoggedInUser *oauth.MultiAccountUser 754 Following []FollowCard 755 Card *ProfileCard 756 Active string 757} 758 759func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 760 params.Active = "overview" 761 return p.executeProfile("user/following", w, params) 762} 763 764type FollowFragmentParams struct { 765 UserDid string 766 FollowStatus models.FollowStatus 767 FollowersCount int64 768} 769 770func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 771 return p.executePlain("user/fragments/follow-oob", w, params) 772} 773 774type ProfilePopoverParams struct { 775 LoggedInUser *oauth.MultiAccountUser 776 UserDid string 777 Profile *models.Profile 778 FollowStatus models.FollowStatus 779 VouchRelationship *models.VouchRelationship 780 Stats ProfilePopoverStats 781} 782 783type ProfilePopoverStats struct { 784 FollowersCount int64 785 FollowingCount int64 786} 787 788func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 789 return p.executePlain("user/fragments/profilePopover", w, params) 790} 791 792type EditBioParams struct { 793 LoggedInUser *oauth.MultiAccountUser 794 Profile *models.Profile 795 AlsoKnownAs []string 796} 797 798func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 799 return p.executePlain("user/fragments/editBio", w, params) 800} 801 802type EditPinsParams struct { 803 LoggedInUser *oauth.MultiAccountUser 804 Profile *models.Profile 805 AllRepos []PinnedRepo 806} 807 808type PinnedRepo struct { 809 IsPinned bool 810 models.Repo 811} 812 813func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 814 return p.executePlain("user/fragments/editPins", w, params) 815} 816 817type StarBtnFragmentParams struct { 818 IsStarred bool 819 SubjectAt syntax.ATURI 820 StarCount int 821 RepoName string 822 HxSwapOob bool 823} 824 825func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 826 params.HxSwapOob = true 827 return p.executePlain("fragments/starBtn", w, params) 828} 829 830type RepoIndexParams struct { 831 LoggedInUser *oauth.MultiAccountUser 832 RepoInfo repoinfo.RepoInfo 833 Active string 834 TagMap map[string][]string 835 CommitsTrunc []types.Commit 836 TagsTrunc []*types.TagReference 837 BranchesTrunc []types.Branch 838 // ForkInfo *types.ForkInfo 839 HTMLReadme template.HTML 840 Raw bool 841 EmailToDid map[string]string 842 VerifiedCommits commitverify.VerifiedCommits 843 Languages []types.RepoLanguageDetails 844 Pipelines map[string]models.Pipeline 845 NeedsKnotUpgrade bool 846 KnotUnreachable bool 847 types.RepoIndexResponse 848} 849 850func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 851 params.Active = "overview" 852 if params.IsEmpty { 853 return p.executeRepo("repo/empty", w, params) 854 } 855 856 if params.NeedsKnotUpgrade { 857 return p.executeRepo("repo/needsUpgrade", w, params) 858 } 859 860 if params.KnotUnreachable { 861 return p.executeRepo("repo/knotUnreachable", w, params) 862 } 863 864 rctx := p.rctx.Clone() 865 rctx.RepoInfo = params.RepoInfo 866 rctx.RepoInfo.Ref = params.Ref 867 rctx.RendererType = markup.RendererTypeRepoMarkdown 868 869 if params.ReadmeFileName != "" { 870 ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 871 switch ext { 872 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 873 params.Raw = false 874 htmlString := rctx.RenderMarkdown(params.Readme) 875 sanitized := rctx.SanitizeDefault(htmlString) 876 params.HTMLReadme = template.HTML(sanitized) 877 default: 878 params.Raw = true 879 } 880 } 881 882 return p.executeRepo("repo/index", w, params) 883} 884 885type RepoLogParams struct { 886 LoggedInUser *oauth.MultiAccountUser 887 RepoInfo repoinfo.RepoInfo 888 TagMap map[string][]string 889 Active string 890 EmailToDid map[string]string 891 VerifiedCommits commitverify.VerifiedCommits 892 Pipelines map[string]models.Pipeline 893 894 types.RepoLogResponse 895} 896 897func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 898 params.Active = "overview" 899 return p.executeRepo("repo/log", w, params) 900} 901 902type RepoCommitParams struct { 903 LoggedInUser *oauth.MultiAccountUser 904 RepoInfo repoinfo.RepoInfo 905 Active string 906 EmailToDid map[string]string 907 Pipeline *models.Pipeline 908 DiffOpts types.DiffOpts 909 910 // singular because it's always going to be just one 911 VerifiedCommit commitverify.VerifiedCommits 912 913 types.RepoCommitResponse 914} 915 916func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 917 params.Active = "overview" 918 return p.executeRepo("repo/commit", w, params) 919} 920 921type RepoTreeParams struct { 922 LoggedInUser *oauth.MultiAccountUser 923 RepoInfo repoinfo.RepoInfo 924 Active string 925 BreadCrumbs [][]string 926 Path string 927 Raw bool 928 HTMLReadme template.HTML 929 EmailToDid map[string]string 930 LastCommitInfo *types.LastCommitInfo 931 types.RepoTreeResponse 932} 933 934type RepoTreeStats struct { 935 NumFolders uint64 936 NumFiles uint64 937} 938 939func (r RepoTreeParams) TreeStats() RepoTreeStats { 940 numFolders, numFiles := 0, 0 941 for _, f := range r.Files { 942 if !f.IsFile() { 943 numFolders += 1 944 } else if f.IsFile() { 945 numFiles += 1 946 } 947 } 948 949 return RepoTreeStats{ 950 NumFolders: uint64(numFolders), 951 NumFiles: uint64(numFiles), 952 } 953} 954 955func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 956 params.Active = "overview" 957 958 rctx := p.rctx.Clone() 959 rctx.RepoInfo = params.RepoInfo 960 rctx.RepoInfo.Ref = params.Ref 961 rctx.RendererType = markup.RendererTypeRepoMarkdown 962 963 if params.ReadmeFileName != "" { 964 ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 965 switch ext { 966 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 967 params.Raw = false 968 htmlString := rctx.RenderMarkdown(params.Readme) 969 sanitized := rctx.SanitizeDefault(htmlString) 970 params.HTMLReadme = template.HTML(sanitized) 971 default: 972 params.Raw = true 973 } 974 } 975 976 return p.executeRepo("repo/tree", w, params) 977} 978 979type RepoBranchesParams struct { 980 LoggedInUser *oauth.MultiAccountUser 981 RepoInfo repoinfo.RepoInfo 982 Active string 983 types.RepoBranchesResponse 984} 985 986func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 987 params.Active = "overview" 988 return p.executeRepo("repo/branches", w, params) 989} 990 991type RepoTagsParams struct { 992 LoggedInUser *oauth.MultiAccountUser 993 RepoInfo repoinfo.RepoInfo 994 Active string 995 types.RepoTagsResponse 996 ArtifactMap map[plumbing.Hash][]models.Artifact 997 DanglingArtifacts []models.Artifact 998} 999 1000func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1001 params.Active = "overview" 1002 return p.executeRepo("repo/tags", w, params) 1003} 1004 1005type RepoTagParams struct { 1006 LoggedInUser *oauth.MultiAccountUser 1007 RepoInfo repoinfo.RepoInfo 1008 Active string 1009 types.RepoTagResponse 1010 ArtifactMap map[plumbing.Hash][]models.Artifact 1011 DanglingArtifacts []models.Artifact 1012} 1013 1014func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1015 params.Active = "overview" 1016 return p.executeRepo("repo/tag", w, params) 1017} 1018 1019type RepoArtifactParams struct { 1020 LoggedInUser *oauth.MultiAccountUser 1021 RepoInfo repoinfo.RepoInfo 1022 Artifact models.Artifact 1023} 1024 1025func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1026 return p.executePlain("repo/fragments/artifact", w, params) 1027} 1028 1029type RepoBlobParams struct { 1030 LoggedInUser *oauth.MultiAccountUser 1031 RepoInfo repoinfo.RepoInfo 1032 Active string 1033 BreadCrumbs [][]string 1034 BlobView models.BlobView 1035 EmailToDid map[string]string 1036 LastCommitInfo *types.LastCommitInfo 1037 *tangled.RepoBlob_Output 1038} 1039 1040func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1041 params.Active = "overview" 1042 return p.executeRepo("repo/blob", w, params) 1043} 1044 1045type Collaborator struct { 1046 Did string 1047 Role string 1048} 1049 1050type RepoSettingsParams struct { 1051 LoggedInUser *oauth.MultiAccountUser 1052 RepoInfo repoinfo.RepoInfo 1053 Collaborators []Collaborator 1054 Active string 1055 Branches []types.Branch 1056 Spindles []string 1057 CurrentSpindle string 1058 Secrets []*tangled.RepoListSecrets_Secret 1059 1060 // TODO: use repoinfo.roles 1061 IsCollaboratorInviteAllowed bool 1062} 1063 1064func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1065 params.Active = "settings" 1066 return p.executeRepo("repo/settings", w, params) 1067} 1068 1069type RepoGeneralSettingsParams struct { 1070 LoggedInUser *oauth.MultiAccountUser 1071 RepoInfo repoinfo.RepoInfo 1072 Labels []models.LabelDefinition 1073 DefaultLabels []models.LabelDefinition 1074 SubscribedLabels map[string]struct{} 1075 ShouldSubscribeAll bool 1076 Active string 1077 Tab string 1078 Branches []types.Branch 1079} 1080 1081func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1082 params.Active = "settings" 1083 params.Tab = "general" 1084 return p.executeRepo("repo/settings/general", w, params) 1085} 1086 1087type RepoAccessSettingsParams struct { 1088 LoggedInUser *oauth.MultiAccountUser 1089 RepoInfo repoinfo.RepoInfo 1090 Active string 1091 Tab string 1092 Collaborators []Collaborator 1093} 1094 1095func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1096 params.Active = "settings" 1097 params.Tab = "access" 1098 return p.executeRepo("repo/settings/access", w, params) 1099} 1100 1101type RepoPipelineSettingsParams struct { 1102 LoggedInUser *oauth.MultiAccountUser 1103 RepoInfo repoinfo.RepoInfo 1104 Active string 1105 Tab string 1106 Spindles []string 1107 CurrentSpindle string 1108 Secrets []map[string]any 1109} 1110 1111func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1112 params.Active = "settings" 1113 params.Tab = "pipelines" 1114 return p.executeRepo("repo/settings/pipelines", w, params) 1115} 1116 1117type RepoWebhooksSettingsParams struct { 1118 LoggedInUser *oauth.MultiAccountUser 1119 RepoInfo repoinfo.RepoInfo 1120 Active string 1121 Tab string 1122 Webhooks []models.Webhook 1123 WebhookDeliveries map[int64][]models.WebhookDelivery 1124} 1125 1126func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1127 params.Active = "settings" 1128 params.Tab = "hooks" 1129 return p.executeRepo("repo/settings/hooks", w, params) 1130} 1131 1132type WebhookDeliveriesListParams struct { 1133 LoggedInUser *oauth.MultiAccountUser 1134 RepoInfo repoinfo.RepoInfo 1135 Webhook *models.Webhook 1136 Deliveries []models.WebhookDelivery 1137} 1138 1139func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1140 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1141 if err != nil { 1142 return err 1143 } 1144 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1145} 1146 1147type RepoSiteSettingsParams struct { 1148 LoggedInUser *oauth.MultiAccountUser 1149 RepoInfo repoinfo.RepoInfo 1150 Active string 1151 Tab string 1152 Branches []types.Branch 1153 SiteConfig *models.RepoSite 1154 OwnerClaim *models.DomainClaim 1155 Deploys []models.SiteDeploy 1156 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1157} 1158 1159func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1160 params.Active = "settings" 1161 params.Tab = "sites" 1162 return p.executeRepo("repo/settings/sites", w, params) 1163} 1164 1165type RepoIssuesParams struct { 1166 LoggedInUser *oauth.MultiAccountUser 1167 RepoInfo repoinfo.RepoInfo 1168 Active string 1169 Issues []models.Issue 1170 IssueCount int 1171 LabelDefs map[string]*models.LabelDefinition 1172 Page pagination.Page 1173 FilterState string 1174 FilterQuery string 1175 BaseFilterQuery string 1176 VouchRelationships map[syntax.DID]*models.VouchRelationship 1177} 1178 1179func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1180 params.Active = "issues" 1181 return p.executeRepo("repo/issues/issues", w, params) 1182} 1183 1184type RepoSingleIssueParams struct { 1185 LoggedInUser *oauth.MultiAccountUser 1186 RepoInfo repoinfo.RepoInfo 1187 Active string 1188 Issue *models.Issue 1189 CommentList []models.CommentListItem 1190 Backlinks []models.RichReferenceLink 1191 LabelDefs map[string]*models.LabelDefinition 1192 1193 Reactions map[models.ReactionKind]models.ReactionDisplayData 1194 UserReacted map[models.ReactionKind]bool 1195 VouchRelationships map[syntax.DID]*models.VouchRelationship 1196} 1197 1198func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1199 params.Active = "issues" 1200 return p.executeRepo("repo/issues/issue", w, params) 1201} 1202 1203type EditIssueParams struct { 1204 LoggedInUser *oauth.MultiAccountUser 1205 RepoInfo repoinfo.RepoInfo 1206 Issue *models.Issue 1207 Action string 1208} 1209 1210func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1211 params.Action = "edit" 1212 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1213} 1214 1215type ThreadReactionFragmentParams struct { 1216 ThreadAt syntax.ATURI 1217 Kind models.ReactionKind 1218 Count int 1219 Users []string 1220 IsReacted bool 1221} 1222 1223func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1224 return p.executePlain("repo/fragments/reaction", w, params) 1225} 1226 1227type RepoNewIssueParams struct { 1228 LoggedInUser *oauth.MultiAccountUser 1229 RepoInfo repoinfo.RepoInfo 1230 Issue *models.Issue // existing issue if any -- passed when editing 1231 Active string 1232 Action string 1233} 1234 1235func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1236 params.Active = "issues" 1237 params.Action = "create" 1238 return p.executeRepo("repo/issues/new", w, params) 1239} 1240 1241type StackedDiff struct { 1242 Diff *types.NiceDiff 1243 Opts types.DiffOpts 1244} 1245 1246type RepoNewPullParams struct { 1247 LoggedInUser *oauth.MultiAccountUser 1248 RepoInfo repoinfo.RepoInfo 1249 Branches []types.Branch 1250 SourceBranches []types.Branch 1251 ForkBranches []types.Branch 1252 Forks []models.Repo 1253 Source Source 1254 SourceBranch string 1255 TargetBranch string 1256 Fork string 1257 Patch string 1258 Title string 1259 Body string 1260 IsStacked bool 1261 Comparison *types.RepoFormatPatchResponse 1262 Diff *types.NiceDiff 1263 DiffOpts types.DiffOpts 1264 StackedDiffs []StackedDiff 1265 MergeCheck *types.MergeCheckResponse 1266 StackTitles map[string]string 1267 StackBodies map[string]string 1268 PrefillError string 1269 Active string 1270 LabelDefs map[string]*models.LabelDefinition 1271 LabelState models.LabelState 1272 StackLabelStates map[string]models.LabelState 1273} 1274 1275func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1276 params.Active = "pulls" 1277 return p.executeRepo("repo/pulls/new", w, params) 1278} 1279 1280func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1281 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1282} 1283 1284func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1285 return p.executePlain("fragments/markdownPreview", w, body) 1286} 1287 1288type RepoPullsParams struct { 1289 LoggedInUser *oauth.MultiAccountUser 1290 RepoInfo repoinfo.RepoInfo 1291 Pulls []*models.Pull 1292 Active string 1293 FilterState string 1294 FilterQuery string 1295 BaseFilterQuery string 1296 Stacks []models.Stack 1297 Pipelines map[string]models.Pipeline 1298 LabelDefs map[string]*models.LabelDefinition 1299 Page pagination.Page 1300 PullCount int 1301 VouchRelationships map[syntax.DID]*models.VouchRelationship 1302} 1303 1304func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1305 params.Active = "pulls" 1306 return p.executeRepo("repo/pulls/pulls", w, params) 1307} 1308 1309type ResubmitResult uint64 1310 1311const ( 1312 ShouldResubmit ResubmitResult = iota 1313 ShouldNotResubmit 1314 Unknown 1315) 1316 1317func (r ResubmitResult) Yes() bool { 1318 return r == ShouldResubmit 1319} 1320func (r ResubmitResult) No() bool { 1321 return r == ShouldNotResubmit 1322} 1323func (r ResubmitResult) Unknown() bool { 1324 return r == Unknown 1325} 1326 1327type RepoSinglePullParams struct { 1328 LoggedInUser *oauth.MultiAccountUser 1329 RepoInfo repoinfo.RepoInfo 1330 Active string 1331 Pull *models.Pull 1332 Stack models.Stack 1333 Backlinks []models.RichReferenceLink 1334 BranchDeleteStatus *models.BranchDeleteStatus 1335 MergeCheck types.MergeCheckResponse 1336 ResubmitCheck ResubmitResult 1337 Pipelines map[string]models.Pipeline 1338 Diff types.DiffRenderer 1339 DiffOpts types.DiffOpts 1340 ActiveRound int 1341 IsInterdiff bool 1342 1343 Reactions map[models.ReactionKind]models.ReactionDisplayData 1344 UserReacted map[models.ReactionKind]bool 1345 1346 LabelDefs map[string]*models.LabelDefinition 1347 VouchRelationships map[syntax.DID]*models.VouchRelationship 1348 VouchSkips map[syntax.DID]bool 1349} 1350 1351func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1352 params.Active = "pulls" 1353 return p.executeRepo("repo/pulls/pull", w, params) 1354} 1355 1356type RepoPullPatchParams struct { 1357 LoggedInUser *oauth.MultiAccountUser 1358 RepoInfo repoinfo.RepoInfo 1359 Pull *models.Pull 1360 Stack models.Stack 1361 Diff *types.NiceDiff 1362 Round int 1363 Submission *models.PullSubmission 1364 DiffOpts types.DiffOpts 1365} 1366 1367// this name is a mouthful 1368func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1369 return p.execute("repo/pulls/patch", w, params) 1370} 1371 1372type RepoPullInterdiffParams struct { 1373 LoggedInUser *oauth.MultiAccountUser 1374 RepoInfo repoinfo.RepoInfo 1375 Pull *models.Pull 1376 Round int 1377 Interdiff *patchutil.InterdiffResult 1378 DiffOpts types.DiffOpts 1379} 1380 1381// this name is a mouthful 1382func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1383 return p.execute("repo/pulls/interdiff", w, params) 1384} 1385 1386type PullResubmitParams struct { 1387 LoggedInUser *oauth.MultiAccountUser 1388 RepoInfo repoinfo.RepoInfo 1389 Pull *models.Pull 1390 SubmissionId int 1391} 1392 1393func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1394 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1395} 1396 1397type PullActionsParams struct { 1398 LoggedInUser *oauth.MultiAccountUser 1399 RepoInfo repoinfo.RepoInfo 1400 Pull *models.Pull 1401 RoundNumber int 1402 MergeCheck types.MergeCheckResponse 1403 ResubmitCheck ResubmitResult 1404 BranchDeleteStatus *models.BranchDeleteStatus 1405 Stack models.Stack 1406} 1407 1408func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1409 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1410} 1411 1412type PullNewCommentParams struct { 1413 LoggedInUser *oauth.MultiAccountUser 1414 RepoInfo repoinfo.RepoInfo 1415 Pull *models.Pull 1416 RoundNumber int 1417} 1418 1419func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1420 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1421} 1422 1423type RepoCompareParams struct { 1424 LoggedInUser *oauth.MultiAccountUser 1425 RepoInfo repoinfo.RepoInfo 1426 Forks []models.Repo 1427 Branches []types.Branch 1428 Tags []*types.TagReference 1429 Base string 1430 Head string 1431 Diff *types.NiceDiff 1432 DiffOpts types.DiffOpts 1433 1434 Active string 1435} 1436 1437func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1438 params.Active = "overview" 1439 return p.executeRepo("repo/compare/compare", w, params) 1440} 1441 1442type RepoCompareNewParams struct { 1443 LoggedInUser *oauth.MultiAccountUser 1444 RepoInfo repoinfo.RepoInfo 1445 Forks []models.Repo 1446 Branches []types.Branch 1447 Tags []*types.TagReference 1448 Base string 1449 Head string 1450 1451 Active string 1452} 1453 1454func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1455 params.Active = "overview" 1456 return p.executeRepo("repo/compare/new", w, params) 1457} 1458 1459type RepoCompareAllowPullParams struct { 1460 LoggedInUser *oauth.MultiAccountUser 1461 RepoInfo repoinfo.RepoInfo 1462 Base string 1463 Head string 1464} 1465 1466func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1467 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1468} 1469 1470type RepoCompareDiffFragmentParams struct { 1471 Diff types.NiceDiff 1472 DiffOpts types.DiffOpts 1473} 1474 1475func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1476 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1477} 1478 1479type LabelPanelParams struct { 1480 LoggedInUser *oauth.MultiAccountUser 1481 RepoInfo repoinfo.RepoInfo 1482 Defs map[string]*models.LabelDefinition 1483 Subject string 1484 State models.LabelState 1485} 1486 1487func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1488 return p.executePlain("repo/fragments/labelPanel", w, params) 1489} 1490 1491type EditLabelPanelParams struct { 1492 LoggedInUser *oauth.MultiAccountUser 1493 RepoInfo repoinfo.RepoInfo 1494 Defs map[string]*models.LabelDefinition 1495 Subject string 1496 State models.LabelState 1497 Prefix string 1498} 1499 1500func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1501 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1502} 1503 1504type RepoStarsParams struct { 1505 LoggedInUser *oauth.MultiAccountUser 1506 RepoInfo repoinfo.RepoInfo 1507 Active string 1508 Starrers []models.Star 1509 Page pagination.Page 1510 TotalCount int 1511} 1512 1513func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1514 params.Active = "overview" 1515 return p.executeRepo("repo/stars", w, params) 1516} 1517 1518type RepoForksParams struct { 1519 LoggedInUser *oauth.MultiAccountUser 1520 RepoInfo repoinfo.RepoInfo 1521 Active string 1522 Forks []models.Repo 1523 Page pagination.Page 1524 TotalCount int 1525} 1526 1527func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1528 params.Active = "overview" 1529 return p.executeRepo("repo/forks", w, params) 1530} 1531 1532type PipelinesParams struct { 1533 LoggedInUser *oauth.MultiAccountUser 1534 RepoInfo repoinfo.RepoInfo 1535 Pipelines []models.Pipeline 1536 Active string 1537 FilterKind string 1538 Total int64 1539} 1540 1541func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1542 params.Active = "pipelines" 1543 return p.executeRepo("repo/pipelines/pipelines", w, params) 1544} 1545 1546type LogBlockParams struct { 1547 Id int 1548 Name string 1549 Command string 1550 Collapsed bool 1551 StartTime time.Time 1552} 1553 1554func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1555 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1556} 1557 1558type LogBlockEndParams struct { 1559 Id int 1560 StartTime time.Time 1561 EndTime time.Time 1562} 1563 1564func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1565 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1566} 1567 1568type LogLineParams struct { 1569 Id int 1570 Content string 1571} 1572 1573func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1574 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1575} 1576 1577type WorkflowParams struct { 1578 LoggedInUser *oauth.MultiAccountUser 1579 RepoInfo repoinfo.RepoInfo 1580 Pipeline models.Pipeline 1581 Workflow string 1582 LogUrl string 1583 Active string 1584} 1585 1586func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1587 params.Active = "pipelines" 1588 return p.executeRepo("repo/pipelines/workflow", w, params) 1589} 1590 1591type PutStringParams struct { 1592 LoggedInUser *oauth.MultiAccountUser 1593 Action string 1594 1595 // this is supplied in the case of editing an existing string 1596 String models.String 1597} 1598 1599func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1600 return p.execute("strings/put", w, params) 1601} 1602 1603type StringsDashboardParams struct { 1604 LoggedInUser *oauth.MultiAccountUser 1605 Card ProfileCard 1606 Strings []models.String 1607} 1608 1609func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1610 return p.execute("strings/dashboard", w, params) 1611} 1612 1613type StringTimelineParams struct { 1614 LoggedInUser *oauth.MultiAccountUser 1615 Strings []models.String 1616} 1617 1618func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1619 return p.execute("strings/timeline", w, params) 1620} 1621 1622type SingleStringParams struct { 1623 LoggedInUser *oauth.MultiAccountUser 1624 ShowRendered bool 1625 RenderToggle bool 1626 RenderedContents template.HTML 1627 String *models.String 1628 Stats models.StringStats 1629 IsStarred bool 1630 StarCount int 1631 Owner identity.Identity 1632} 1633 1634func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1635 return p.execute("strings/string", w, params) 1636} 1637 1638type SearchReposParams struct { 1639 LoggedInUser *oauth.MultiAccountUser 1640 Repos []models.Repo 1641 Page pagination.Page 1642 ResultCount int 1643 FilterQuery string 1644 SortParam string 1645 TimeTaken time.Duration 1646 DocCount int64 1647} 1648 1649func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1650 return p.execute("search/search", w, params) 1651} 1652 1653type SearchQuickParams struct { 1654 Repos []models.Repo 1655 Query string 1656 Total int 1657} 1658 1659func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1660 return p.executePlain("search/fragments/quick", w, params) 1661} 1662 1663func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1664 tpl, err := p.parse("search/fragments/quick") 1665 if err != nil { 1666 return err 1667 } 1668 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1669} 1670 1671func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1672 return p.execute("timeline/home", w, params) 1673} 1674 1675type CommentBodyFragmentParams struct { 1676 Comment models.Comment 1677 Reactions map[models.ReactionKind]models.ReactionDisplayData 1678 UserReacted map[models.ReactionKind]bool 1679} 1680 1681func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1682 return p.executePlain("fragments/comment/commentBody", w, params) 1683} 1684 1685type EditCommentFragmentParams struct { 1686 Comment models.Comment 1687} 1688 1689func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1690 return p.executePlain("fragments/comment/edit", w, params) 1691} 1692 1693type ReplyCommentFragmentParams struct { 1694 LoggedInUser *oauth.MultiAccountUser 1695} 1696 1697func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1698 return p.executePlain("fragments/comment/reply", w, params) 1699} 1700 1701type ReplyPlaceholderFragmentParams struct { 1702 LoggedInUser *oauth.MultiAccountUser 1703} 1704 1705func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1706 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1707} 1708 1709func (p *Pages) Static() http.Handler { 1710 if p.dev { 1711 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1712 } 1713 1714 sub, err := fs.Sub(p.embedFS, "static") 1715 if err != nil { 1716 p.logger.Error("no static dir found? that's crazy", "err", err) 1717 panic(err) 1718 } 1719 // Custom handler to apply Cache-Control headers for font files 1720 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1721} 1722 1723func Cache(h http.Handler) http.Handler { 1724 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1725 path := strings.Split(r.URL.Path, "?")[0] 1726 1727 if strings.HasSuffix(path, ".css") { 1728 // on day for css files 1729 w.Header().Set("Cache-Control", "public, max-age=86400") 1730 } else { 1731 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1732 } 1733 h.ServeHTTP(w, r) 1734 }) 1735} 1736 1737func (p *Pages) CssContentHash() string { 1738 cssFile, err := p.embedFS.Open("static/tw.css") 1739 if err != nil { 1740 slog.Debug("Error opening CSS file", "err", err) 1741 return "" 1742 } 1743 defer cssFile.Close() 1744 1745 hasher := sha256.New() 1746 if _, err := io.Copy(hasher, cssFile); err != nil { 1747 slog.Debug("Error hashing CSS file", "err", err) 1748 return "" 1749 } 1750 1751 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1752} 1753 1754func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1755 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1756} 1757 1758func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1759 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1760} 1761 1762func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1763 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1764} 1765 1766func (p *Pages) Error500(w io.Writer) error { 1767 return p.execute("errors/500", w, nil) 1768} 1769 1770func (p *Pages) Error404(w io.Writer) error { 1771 return p.execute("errors/404", w, nil) 1772} 1773 1774func (p *Pages) ErrorKnot404(w io.Writer) error { 1775 return p.execute("errors/knot404", w, nil) 1776} 1777 1778func (p *Pages) Error503(w io.Writer) error { 1779 return p.execute("errors/503", w, nil) 1780}