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