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
48 kB 1784 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[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1194 UserReacted map[syntax.ATURI]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 Kind models.ReactionKind 1217 Count int 1218 Users []string 1219 IsReacted bool 1220} 1221 1222func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1223 return p.executePlain("repo/fragments/reaction", w, params) 1224} 1225 1226type RepoNewIssueParams struct { 1227 LoggedInUser *oauth.MultiAccountUser 1228 RepoInfo repoinfo.RepoInfo 1229 Issue *models.Issue // existing issue if any -- passed when editing 1230 Active string 1231 Action string 1232} 1233 1234func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1235 params.Active = "issues" 1236 params.Action = "create" 1237 return p.executeRepo("repo/issues/new", w, params) 1238} 1239 1240type StackedDiff struct { 1241 Diff *types.NiceDiff 1242 Opts types.DiffOpts 1243} 1244 1245type RepoNewPullParams struct { 1246 LoggedInUser *oauth.MultiAccountUser 1247 RepoInfo repoinfo.RepoInfo 1248 Branches []types.Branch 1249 SourceBranches []types.Branch 1250 ForkBranches []types.Branch 1251 Forks []models.Repo 1252 Source Source 1253 SourceBranch string 1254 TargetBranch string 1255 Fork string 1256 Patch string 1257 Title string 1258 Body string 1259 IsStacked bool 1260 Comparison *types.RepoFormatPatchResponse 1261 Diff *types.NiceDiff 1262 DiffOpts types.DiffOpts 1263 StackedDiffs []StackedDiff 1264 MergeCheck *types.MergeCheckResponse 1265 StackTitles map[string]string 1266 StackBodies map[string]string 1267 PrefillError string 1268 Active string 1269 LabelDefs map[string]*models.LabelDefinition 1270 LabelState models.LabelState 1271 StackLabelStates map[string]models.LabelState 1272} 1273 1274func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1275 params.Active = "pulls" 1276 return p.executeRepo("repo/pulls/new", w, params) 1277} 1278 1279func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1280 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1281} 1282 1283func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1284 return p.executePlain("fragments/markdownPreview", w, body) 1285} 1286 1287type RepoPullsParams struct { 1288 LoggedInUser *oauth.MultiAccountUser 1289 RepoInfo repoinfo.RepoInfo 1290 Pulls []*models.Pull 1291 Active string 1292 FilterState string 1293 FilterQuery string 1294 BaseFilterQuery string 1295 Stacks []models.Stack 1296 Pipelines map[string]models.Pipeline 1297 LabelDefs map[string]*models.LabelDefinition 1298 Page pagination.Page 1299 PullCount int 1300 VouchRelationships map[syntax.DID]*models.VouchRelationship 1301} 1302 1303func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1304 params.Active = "pulls" 1305 return p.executeRepo("repo/pulls/pulls", w, params) 1306} 1307 1308type ResubmitResult uint64 1309 1310const ( 1311 ShouldResubmit ResubmitResult = iota 1312 ShouldNotResubmit 1313 Unknown 1314) 1315 1316func (r ResubmitResult) Yes() bool { 1317 return r == ShouldResubmit 1318} 1319func (r ResubmitResult) No() bool { 1320 return r == ShouldNotResubmit 1321} 1322func (r ResubmitResult) Unknown() bool { 1323 return r == Unknown 1324} 1325 1326type RepoSinglePullParams struct { 1327 LoggedInUser *oauth.MultiAccountUser 1328 RepoInfo repoinfo.RepoInfo 1329 Active string 1330 Pull *models.Pull 1331 Stack models.Stack 1332 Backlinks []models.RichReferenceLink 1333 BranchDeleteStatus *models.BranchDeleteStatus 1334 MergeCheck types.MergeCheckResponse 1335 ResubmitCheck ResubmitResult 1336 Pipelines map[string]models.Pipeline 1337 Diff types.DiffRenderer 1338 DiffOpts types.DiffOpts 1339 ActiveRound int 1340 IsInterdiff bool 1341 1342 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1343 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1344 1345 LabelDefs map[string]*models.LabelDefinition 1346 VouchRelationships map[syntax.DID]*models.VouchRelationship 1347 VouchSkips map[syntax.DID]bool 1348} 1349 1350func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1351 params.Active = "pulls" 1352 return p.executeRepo("repo/pulls/pull", w, params) 1353} 1354 1355type RepoPullPatchParams struct { 1356 LoggedInUser *oauth.MultiAccountUser 1357 RepoInfo repoinfo.RepoInfo 1358 Pull *models.Pull 1359 Stack models.Stack 1360 Diff *types.NiceDiff 1361 Round int 1362 Submission *models.PullSubmission 1363 DiffOpts types.DiffOpts 1364} 1365 1366// this name is a mouthful 1367func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1368 return p.execute("repo/pulls/patch", w, params) 1369} 1370 1371type RepoPullInterdiffParams struct { 1372 LoggedInUser *oauth.MultiAccountUser 1373 RepoInfo repoinfo.RepoInfo 1374 Pull *models.Pull 1375 Round int 1376 Interdiff *patchutil.InterdiffResult 1377 DiffOpts types.DiffOpts 1378} 1379 1380// this name is a mouthful 1381func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1382 return p.execute("repo/pulls/interdiff", w, params) 1383} 1384 1385type PullResubmitParams struct { 1386 LoggedInUser *oauth.MultiAccountUser 1387 RepoInfo repoinfo.RepoInfo 1388 Pull *models.Pull 1389 SubmissionId int 1390} 1391 1392func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1393 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1394} 1395 1396type PullActionsParams struct { 1397 LoggedInUser *oauth.MultiAccountUser 1398 RepoInfo repoinfo.RepoInfo 1399 Pull *models.Pull 1400 RoundNumber int 1401 MergeCheck types.MergeCheckResponse 1402 ResubmitCheck ResubmitResult 1403 BranchDeleteStatus *models.BranchDeleteStatus 1404 Stack models.Stack 1405} 1406 1407func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1408 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1409} 1410 1411type PullNewCommentParams struct { 1412 LoggedInUser *oauth.MultiAccountUser 1413 RepoInfo repoinfo.RepoInfo 1414 Pull *models.Pull 1415 RoundNumber int 1416} 1417 1418func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1419 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1420} 1421 1422type RepoCompareParams struct { 1423 LoggedInUser *oauth.MultiAccountUser 1424 RepoInfo repoinfo.RepoInfo 1425 Forks []models.Repo 1426 Branches []types.Branch 1427 Tags []*types.TagReference 1428 Base string 1429 Head string 1430 Diff *types.NiceDiff 1431 DiffOpts types.DiffOpts 1432 1433 Active string 1434} 1435 1436func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1437 params.Active = "overview" 1438 return p.executeRepo("repo/compare/compare", w, params) 1439} 1440 1441type RepoCompareNewParams struct { 1442 LoggedInUser *oauth.MultiAccountUser 1443 RepoInfo repoinfo.RepoInfo 1444 Forks []models.Repo 1445 Branches []types.Branch 1446 Tags []*types.TagReference 1447 Base string 1448 Head string 1449 1450 Active string 1451} 1452 1453func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1454 params.Active = "overview" 1455 return p.executeRepo("repo/compare/new", w, params) 1456} 1457 1458type RepoCompareAllowPullParams struct { 1459 LoggedInUser *oauth.MultiAccountUser 1460 RepoInfo repoinfo.RepoInfo 1461 Base string 1462 Head string 1463} 1464 1465func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1466 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1467} 1468 1469type RepoCompareDiffFragmentParams struct { 1470 Diff types.NiceDiff 1471 DiffOpts types.DiffOpts 1472} 1473 1474func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1475 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1476} 1477 1478type LabelPanelParams struct { 1479 LoggedInUser *oauth.MultiAccountUser 1480 RepoInfo repoinfo.RepoInfo 1481 Defs map[string]*models.LabelDefinition 1482 Subject string 1483 State models.LabelState 1484} 1485 1486func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1487 return p.executePlain("repo/fragments/labelPanel", w, params) 1488} 1489 1490type EditLabelPanelParams struct { 1491 LoggedInUser *oauth.MultiAccountUser 1492 RepoInfo repoinfo.RepoInfo 1493 Defs map[string]*models.LabelDefinition 1494 Subject string 1495 State models.LabelState 1496 Prefix string 1497} 1498 1499func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1500 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1501} 1502 1503type RepoStarsParams struct { 1504 LoggedInUser *oauth.MultiAccountUser 1505 RepoInfo repoinfo.RepoInfo 1506 Active string 1507 Starrers []models.Star 1508 Page pagination.Page 1509 TotalCount int 1510} 1511 1512func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1513 params.Active = "overview" 1514 return p.executeRepo("repo/stars", w, params) 1515} 1516 1517type RepoForksParams struct { 1518 LoggedInUser *oauth.MultiAccountUser 1519 RepoInfo repoinfo.RepoInfo 1520 Active string 1521 Forks []models.Repo 1522 Page pagination.Page 1523 TotalCount int 1524} 1525 1526func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1527 params.Active = "overview" 1528 return p.executeRepo("repo/forks", w, params) 1529} 1530 1531type PipelinesParams struct { 1532 LoggedInUser *oauth.MultiAccountUser 1533 RepoInfo repoinfo.RepoInfo 1534 Pipelines []models.Pipeline 1535 Active string 1536 FilterKind string 1537 Total int64 1538} 1539 1540func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1541 params.Active = "pipelines" 1542 return p.executeRepo("repo/pipelines/pipelines", w, params) 1543} 1544 1545type LogBlockParams struct { 1546 Id int 1547 Name string 1548 Command string 1549 Collapsed bool 1550 StartTime time.Time 1551} 1552 1553func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1554 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1555} 1556 1557type LogBlockEndParams struct { 1558 Id int 1559 StartTime time.Time 1560 EndTime time.Time 1561} 1562 1563func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1564 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1565} 1566 1567type LogLineParams struct { 1568 Id int 1569 Content string 1570} 1571 1572func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1573 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1574} 1575 1576type WorkflowParams struct { 1577 LoggedInUser *oauth.MultiAccountUser 1578 RepoInfo repoinfo.RepoInfo 1579 Pipeline models.Pipeline 1580 Workflow string 1581 LogUrl string 1582 Active string 1583} 1584 1585func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1586 params.Active = "pipelines" 1587 return p.executeRepo("repo/pipelines/workflow", w, params) 1588} 1589 1590type PutStringParams struct { 1591 LoggedInUser *oauth.MultiAccountUser 1592 Action string 1593 1594 // this is supplied in the case of editing an existing string 1595 String models.String 1596} 1597 1598func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1599 return p.execute("strings/put", w, params) 1600} 1601 1602type StringsDashboardParams struct { 1603 LoggedInUser *oauth.MultiAccountUser 1604 Card ProfileCard 1605 Strings []models.String 1606} 1607 1608func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1609 return p.execute("strings/dashboard", w, params) 1610} 1611 1612type StringTimelineParams struct { 1613 LoggedInUser *oauth.MultiAccountUser 1614 Strings []models.String 1615} 1616 1617func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1618 return p.execute("strings/timeline", w, params) 1619} 1620 1621type SingleStringParams struct { 1622 LoggedInUser *oauth.MultiAccountUser 1623 ShowRendered bool 1624 RenderToggle bool 1625 RenderedContents template.HTML 1626 String *models.String 1627 Stats models.StringStats 1628 IsStarred bool 1629 StarCount int 1630 Owner identity.Identity 1631 CommentList []models.CommentListItem 1632 1633 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1634 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1635 VouchRelationships map[syntax.DID]*models.VouchRelationship 1636} 1637 1638func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1639 return p.execute("strings/string", w, params) 1640} 1641 1642type SearchReposParams struct { 1643 LoggedInUser *oauth.MultiAccountUser 1644 Repos []models.Repo 1645 Page pagination.Page 1646 ResultCount int 1647 FilterQuery string 1648 SortParam string 1649 TimeTaken time.Duration 1650 DocCount int64 1651} 1652 1653func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1654 return p.execute("search/search", w, params) 1655} 1656 1657type SearchQuickParams struct { 1658 Repos []models.Repo 1659 Query string 1660 Total int 1661} 1662 1663func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1664 return p.executePlain("search/fragments/quick", w, params) 1665} 1666 1667func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1668 tpl, err := p.parse("search/fragments/quick") 1669 if err != nil { 1670 return err 1671 } 1672 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1673} 1674 1675func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1676 return p.execute("timeline/home", w, params) 1677} 1678 1679type CommentBodyFragmentParams struct { 1680 Comment models.Comment 1681 Reactions map[models.ReactionKind]models.ReactionDisplayData 1682 UserReacted map[models.ReactionKind]bool 1683} 1684 1685func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1686 return p.executePlain("fragments/comment/commentBody", w, params) 1687} 1688 1689type EditCommentFragmentParams struct { 1690 Comment models.Comment 1691} 1692 1693func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1694 return p.executePlain("fragments/comment/edit", w, params) 1695} 1696 1697type ReplyCommentFragmentParams struct { 1698 LoggedInUser *oauth.MultiAccountUser 1699} 1700 1701func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1702 return p.executePlain("fragments/comment/reply", w, params) 1703} 1704 1705type ReplyPlaceholderFragmentParams struct { 1706 LoggedInUser *oauth.MultiAccountUser 1707} 1708 1709func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1710 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1711} 1712 1713func (p *Pages) Static() http.Handler { 1714 if p.dev { 1715 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1716 } 1717 1718 sub, err := fs.Sub(p.embedFS, "static") 1719 if err != nil { 1720 p.logger.Error("no static dir found? that's crazy", "err", err) 1721 panic(err) 1722 } 1723 // Custom handler to apply Cache-Control headers for font files 1724 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1725} 1726 1727func Cache(h http.Handler) http.Handler { 1728 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1729 path := strings.Split(r.URL.Path, "?")[0] 1730 1731 if strings.HasSuffix(path, ".css") { 1732 // on day for css files 1733 w.Header().Set("Cache-Control", "public, max-age=86400") 1734 } else { 1735 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1736 } 1737 h.ServeHTTP(w, r) 1738 }) 1739} 1740 1741func (p *Pages) CssContentHash() string { 1742 cssFile, err := p.embedFS.Open("static/tw.css") 1743 if err != nil { 1744 slog.Debug("Error opening CSS file", "err", err) 1745 return "" 1746 } 1747 defer cssFile.Close() 1748 1749 hasher := sha256.New() 1750 if _, err := io.Copy(hasher, cssFile); err != nil { 1751 slog.Debug("Error hashing CSS file", "err", err) 1752 return "" 1753 } 1754 1755 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1756} 1757 1758func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1759 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1760} 1761 1762func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1763 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1764} 1765 1766func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1767 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1768} 1769 1770func (p *Pages) Error500(w io.Writer) error { 1771 return p.execute("errors/500", w, nil) 1772} 1773 1774func (p *Pages) Error404(w io.Writer) error { 1775 return p.execute("errors/404", w, nil) 1776} 1777 1778func (p *Pages) ErrorKnot404(w io.Writer) error { 1779 return p.execute("errors/knot404", w, nil) 1780} 1781 1782func (p *Pages) Error503(w io.Writer) error { 1783 return p.execute("errors/503", w, nil) 1784}