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
55 kB 2123 lines
1package pages 2 3import ( 4 "context" 5 "crypto/sha256" 6 "embed" 7 "encoding/hex" 8 "fmt" 9 "html/template" 10 "io" 11 "io/fs" 12 "log/slog" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 "sync" 18 "time" 19 20 "tangled.org/core/api/tangled" 21 "tangled.org/core/appview/cache" 22 "tangled.org/core/appview/commitverify" 23 "tangled.org/core/appview/config" 24 "tangled.org/core/appview/db" 25 "tangled.org/core/appview/models" 26 "tangled.org/core/appview/oauth" 27 "tangled.org/core/appview/pages/markup" 28 "tangled.org/core/appview/pages/markup/sanitizer" 29 "tangled.org/core/appview/pages/repoinfo" 30 "tangled.org/core/appview/pagination" 31 "tangled.org/core/idresolver" 32 "tangled.org/core/types" 33 34 "github.com/bluesky-social/indigo/atproto/identity" 35 "github.com/bluesky-social/indigo/atproto/syntax" 36 "github.com/go-git/go-git/v5/plumbing" 37 "github.com/sourcegraph/zoekt" 38) 39 40//go:embed templates/* static legal 41var Files embed.FS 42 43type baseParamsCtxKey struct{} 44 45type BaseParams struct { 46 LoggedInUser *oauth.MultiAccountUser 47 FocusParams FocusParams 48 ThemePreference models.ThemePreference 49} 50 51type FocusParams struct { 52 Focusing bool 53 FocusLink string 54 FocusNotificationID int64 55 CurrentPath string // r.URL.Path, for off-focus detection in templates 56 FocusCount int // total unread focus-eligible items remaining 57} 58 59func (p *Pages) Resolver() *idresolver.Resolver { 60 return p.resolver 61} 62 63func BaseParamsIntoContext(ctx context.Context, bp BaseParams) context.Context { 64 return context.WithValue(ctx, baseParamsCtxKey{}, bp) 65} 66 67func BaseParamsFromContext(ctx context.Context) BaseParams { 68 bp, _ := ctx.Value(baseParamsCtxKey{}).(BaseParams) 69 return bp 70} 71 72type Pages struct { 73 mu sync.RWMutex 74 cache *TmplCache[string, *template.Template] 75 76 avatar config.AvatarConfig 77 pdsCfg config.PdsConfig 78 resolver *idresolver.Resolver 79 db *db.DB 80 rdb *cache.Cache 81 dev bool 82 embedFS fs.FS 83 templateDir string // Path to templates on disk for dev mode 84 rctx *markup.RenderContext 85 logger *slog.Logger 86} 87 88func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 89 // initialized with safe defaults, can be overridden per use 90 rctx := &markup.RenderContext{ 91 IsDev: config.Core.Dev, 92 Hostname: config.Core.AppviewHost, 93 CamoUrl: config.Camo.Host, 94 CamoSecret: config.Camo.SharedSecret, 95 Files: Files, 96 } 97 98 p := &Pages{ 99 mu: sync.RWMutex{}, 100 cache: NewTmplCache[string, *template.Template](), 101 dev: config.Core.Dev, 102 avatar: config.Avatar, 103 pdsCfg: config.Pds, 104 rctx: rctx, 105 resolver: res, 106 db: database, 107 rdb: rdb, 108 templateDir: "appview/pages", 109 logger: logger, 110 } 111 112 if p.dev { 113 p.embedFS = os.DirFS(p.templateDir) 114 } else { 115 p.embedFS = Files 116 } 117 118 return p 119} 120 121// reverse of pathToName 122func (p *Pages) nameToPath(s string) string { 123 return "templates/" + s + ".html" 124} 125 126// FuncMap returns the template function map for use by external template consumers. 127func (p *Pages) FuncMap() template.FuncMap { 128 return p.funcMap() 129} 130 131// FragmentPaths returns all fragment template paths from the embedded FS. 132func (p *Pages) FragmentPaths() ([]string, error) { 133 return p.fragmentPaths() 134} 135 136// EmbedFS returns the embedded filesystem containing templates and static assets. 137func (p *Pages) EmbedFS() fs.FS { 138 return p.embedFS 139} 140 141func (p *Pages) fragmentPaths() ([]string, error) { 142 var fragmentPaths []string 143 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 144 if err != nil { 145 return err 146 } 147 if d.IsDir() { 148 return nil 149 } 150 if !strings.HasSuffix(path, ".html") { 151 return nil 152 } 153 if !strings.Contains(path, "fragments/") { 154 return nil 155 } 156 fragmentPaths = append(fragmentPaths, path) 157 return nil 158 }) 159 if err != nil { 160 return nil, err 161 } 162 163 return fragmentPaths, nil 164} 165 166// parse without memoization 167func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 168 paths, err := p.fragmentPaths() 169 if err != nil { 170 return nil, err 171 } 172 for _, s := range stack { 173 paths = append(paths, p.nameToPath(s)) 174 } 175 176 funcs := p.funcMap() 177 top := stack[len(stack)-1] 178 parsed, err := template.New(top). 179 Funcs(funcs). 180 ParseFS(p.embedFS, paths...) 181 if err != nil { 182 return nil, err 183 } 184 185 return parsed, nil 186} 187 188func (p *Pages) parse(stack ...string) (*template.Template, error) { 189 key := strings.Join(stack, "|") 190 191 // never cache in dev mode 192 if cached, exists := p.cache.Get(key); !p.dev && exists { 193 return cached, nil 194 } 195 196 result, err := p.rawParse(stack...) 197 if err != nil { 198 return nil, err 199 } 200 201 p.cache.Set(key, result) 202 return result, nil 203} 204 205func (p *Pages) parseBase(top string) (*template.Template, error) { 206 stack := []string{ 207 "layouts/base", 208 top, 209 } 210 return p.parse(stack...) 211} 212 213func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 214 stack := []string{ 215 "layouts/base", 216 "layouts/repobase", 217 top, 218 } 219 return p.parse(stack...) 220} 221 222func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 223 stack := []string{ 224 "layouts/base", 225 "layouts/profilebase", 226 top, 227 } 228 return p.parse(stack...) 229} 230 231func (p *Pages) parseLoginBase(top string) (*template.Template, error) { 232 stack := []string{ 233 "layouts/base", 234 "layouts/loginbase", 235 top, 236 } 237 return p.parse(stack...) 238} 239 240func (p *Pages) parseOnboardingBase(top string) (*template.Template, error) { 241 stack := []string{ 242 "layouts/base", 243 "layouts/onboardingbase", 244 top, 245 } 246 return p.parse(stack...) 247} 248 249func (p *Pages) executePlain(name string, w io.Writer, params any) error { 250 tpl, err := p.parse(name) 251 if err != nil { 252 return err 253 } 254 255 err = tpl.Execute(w, params) 256 if err != nil { 257 p.logger.Error("failed to execute template", "template", name, "err", err) 258 } 259 return err 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 err = tpl.ExecuteTemplate(w, "layouts/base", params) 269 if err != nil { 270 p.logger.Error("failed to execute login template", "template", name, "err", err) 271 } 272 return err 273} 274 275func (p *Pages) executeOnboarding(name string, w io.Writer, params any) error { 276 tpl, err := p.parseOnboardingBase(name) 277 if err != nil { 278 return err 279 } 280 281 return tpl.ExecuteTemplate(w, "layouts/base", params) 282} 283 284func (p *Pages) execute(name string, w io.Writer, params any) error { 285 tpl, err := p.parseBase(name) 286 if err != nil { 287 return err 288 } 289 290 err = tpl.ExecuteTemplate(w, "layouts/base", params) 291 if err != nil { 292 p.logger.Error("failed to execute template", "template", name, "err", err) 293 } 294 return err 295} 296 297func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 298 tpl, err := p.parseRepoBase(name) 299 if err != nil { 300 return err 301 } 302 303 err = tpl.ExecuteTemplate(w, "layouts/base", params) 304 if err != nil { 305 p.logger.Error("failed to execute repo template", "template", name, "err", err) 306 } 307 return err 308} 309 310func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 311 tpl, err := p.parseProfileBase(name) 312 if err != nil { 313 return err 314 } 315 316 err = tpl.ExecuteTemplate(w, "layouts/base", params) 317 if err != nil { 318 p.logger.Error("failed to execute profile template", "template", name, "err", err) 319 } 320 return err 321} 322 323type DollyParams struct { 324 Classes string 325 FillColor string 326 // Favicon embeds a prefers-color-scheme style block so the SVG 327 // adapts to dark mode when used as a standalone favicon document. 328 Favicon bool 329} 330 331func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 332 return p.executePlain("fragments/dolly/logo", w, params) 333} 334 335func (p *Pages) Favicon(w io.Writer) error { 336 return p.Dolly(w, DollyParams{ 337 Favicon: true, 338 }) 339} 340 341type LoginParams struct { 342 BaseParams 343 ReturnUrl string 344 ErrorCode string 345 AddAccount bool 346 Handle string 347 Accounts []oauth.AccountInfo 348} 349 350func (p *Pages) Login(w io.Writer, params LoginParams) error { 351 return p.executeLogin("user/login", w, params) 352} 353 354type SignupParams struct { 355 BaseParams 356 CloudflareSiteKey string 357 EmailId string 358} 359 360func (p *Pages) Signup(w io.Writer, params SignupParams) error { 361 return p.executeLogin("user/signup", w, params) 362} 363 364func (p *Pages) CompleteSignup(w io.Writer) error { 365 return p.executeLogin("user/completeSignup", w, BaseParams{}) 366} 367 368type SignupSuccessParams struct { 369 Handle string 370} 371 372func (p *Pages) SignupSuccess(w io.Writer, params SignupSuccessParams) error { 373 return p.executePlain("user/fragments/signupSuccess", w, params) 374} 375 376type TermsOfServiceParams struct { 377 BaseParams 378 Content template.HTML 379} 380 381func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 382 filename := "terms.md" 383 filePath := filepath.Join("legal", filename) 384 385 file, err := p.embedFS.Open(filePath) 386 if err != nil { 387 return fmt.Errorf("failed to read %s: %w", filename, err) 388 } 389 defer file.Close() 390 391 markdownBytes, err := io.ReadAll(file) 392 if err != nil { 393 return fmt.Errorf("failed to read %s: %w", filename, err) 394 } 395 396 rctx := p.rctx.Clone() 397 rctx.RendererType = markup.RendererTypeDefault 398 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 399 sanitized := sanitizer.SanitizeDefault(htmlString) 400 params.Content = template.HTML(sanitized) 401 402 return p.execute("legal/terms", w, params) 403} 404 405type PrivacyPolicyParams struct { 406 BaseParams 407 Content template.HTML 408} 409 410func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 411 filename := "privacy.md" 412 filePath := filepath.Join("legal", filename) 413 414 file, err := p.embedFS.Open(filePath) 415 if err != nil { 416 return fmt.Errorf("failed to read %s: %w", filename, err) 417 } 418 defer file.Close() 419 420 markdownBytes, err := io.ReadAll(file) 421 if err != nil { 422 return fmt.Errorf("failed to read %s: %w", filename, err) 423 } 424 425 rctx := p.rctx.Clone() 426 rctx.RendererType = markup.RendererTypeDefault 427 htmlString := rctx.RenderMarkdown(string(markdownBytes)) 428 sanitized := sanitizer.SanitizeDefault(htmlString) 429 params.Content = template.HTML(sanitized) 430 431 return p.execute("legal/privacy", w, params) 432} 433 434type BrandParams struct { 435 BaseParams 436} 437 438func (p *Pages) Brand(w io.Writer, params BrandParams) error { 439 return p.execute("brand/brand", w, params) 440} 441 442type RecentItem struct { 443 Link *models.RecentLink 444 Repo *models.Repo 445 Issue *models.Issue 446 Pull *models.Pull 447} 448 449type BlogPost struct { 450 Slug string 451 Title string 452 Subtitle string 453 Date time.Time 454} 455 456type TimelineParams struct { 457 BaseParams 458 Onboarding models.OnboardingProgress 459 Timeline []models.TimelineGroup 460 Repos []models.Repo 461 GfiLabel *models.LabelDefinition 462 BlueskyPosts []models.BskyPost 463 VouchSuggestions []models.VouchSuggestion 464 Notifications []*models.NotificationWithEntity 465 Recents []RecentItem 466 FollowingOnly bool 467 RecentBlogPosts []BlogPost 468 // ShowNewsletter controls whether the newsletter widget/CTA is rendered. 469 // For logged-in users it reflects their newsletter_preferences row; for 470 // anonymous visitors it is always true (dismissal falls back to 471 // localStorage on the client). 472 ShowNewsletter bool 473 CanFocus bool 474} 475 476func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 477 return p.execute("timeline/timeline", w, params) 478} 479 480type GoodFirstIssuesParams struct { 481 BaseParams 482 Issues []models.Issue 483 RepoGroups []*models.RepoGroup 484 LabelDefs map[string]*models.LabelDefinition 485 GfiLabel *models.LabelDefinition 486 Page pagination.Page 487} 488 489func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 490 return p.execute("goodfirstissues/index", w, params) 491} 492 493type UserProfileSettingsParams struct { 494 BaseParams 495 Tab string 496 PunchcardPreference models.PunchcardPreference 497 IsTnglSh bool 498 IsDeactivated bool 499 HandleOpen bool 500} 501 502func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 503 params.Tab = "profile" 504 return p.execute("user/settings/profile", w, params) 505} 506 507type GroupedNotifications struct { 508 Today []*models.NotificationWithEntity 509 ThisWeek []*models.NotificationWithEntity 510 Older []*models.NotificationWithEntity 511} 512 513func GroupNotificationsByDate(notifs []*models.NotificationWithEntity) GroupedNotifications { 514 now := time.Now() 515 todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 516 weekStart := todayStart.AddDate(0, 0, -6) 517 518 var g GroupedNotifications 519 for _, n := range notifs { 520 switch { 521 case !n.Created.Before(todayStart): 522 g.Today = append(g.Today, n) 523 case !n.Created.Before(weekStart): 524 g.ThisWeek = append(g.ThisWeek, n) 525 default: 526 g.Older = append(g.Older, n) 527 } 528 } 529 return g 530} 531 532type NotificationsParams struct { 533 BaseParams 534 WorkGroups GroupedNotifications 535 SocialGroups GroupedNotifications 536 MobileGroups GroupedNotifications 537 WorkUnreadCount int64 538 SocialUnreadCount int64 539 Page pagination.Page 540 Total int 541 ReadFilter string // "inbox" or "unread" 542 CategoryFilter string // "all", "work", "social" 543 CanFocus bool 544} 545 546func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 547 return p.execute("notifications/list", w, params) 548} 549 550func (p *Pages) NotificationItem(w io.Writer, notif *models.NotificationWithEntity) error { 551 return p.executePlain("notifications/fragments/item", w, notif) 552} 553 554type NotificationCountParams struct { 555 Count int64 556} 557 558func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 559 return p.executePlain("notifications/fragments/count", w, params) 560} 561 562type NotificationPreviewParams struct { 563 BaseParams 564 Notifications []*models.NotificationWithEntity 565 ReadFilter string 566 CategoryFilter string 567 CanFocus bool 568} 569 570func (p *Pages) NotificationPreview(w io.Writer, params NotificationPreviewParams) error { 571 return p.executePlain("notifications/fragments/preview", w, params) 572} 573 574type UserKeysSettingsParams struct { 575 BaseParams 576 PubKeys []models.PublicKey 577 Tab string 578} 579 580func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 581 params.Tab = "keys" 582 return p.execute("user/settings/keys", w, params) 583} 584 585type UserEmailsSettingsParams struct { 586 BaseParams 587 Emails []models.Email 588 Tab string 589} 590 591func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 592 params.Tab = "emails" 593 return p.execute("user/settings/emails", w, params) 594} 595 596type UserNotificationSettingsParams struct { 597 BaseParams 598 Preferences *models.NotificationPreferences 599 HasVerifiedEmail bool 600 Tab string 601} 602 603func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 604 params.Tab = "notifications" 605 return p.execute("user/settings/notifications", w, params) 606} 607 608type UserSiteSettingsParams struct { 609 BaseParams 610 Claim *models.DomainClaim 611 SitesDomain string 612 IsTnglHandle bool 613 Tab string 614} 615 616func (p *Pages) UserSiteSettings(w io.Writer, params UserSiteSettingsParams) error { 617 params.Tab = "sites" 618 return p.execute("user/settings/sites", w, params) 619} 620 621type UpgradeBannerParams struct { 622 Registrations []models.Registration 623 Spindles []models.Spindle 624} 625 626func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 627 return p.executePlain("banner", w, params) 628} 629 630type NewsletterResponseParams struct { 631 // Id identifies the calling form instance; the response span's id will 632 // be "newsletter-msg-<Id>" so it round-trips with the form's hx-target. 633 Id string 634 // Error, when non-empty, switches the template to the error variant. 635 Error string 636} 637 638func (p *Pages) NewsletterResponse(w io.Writer, params NewsletterResponseParams) error { 639 return p.executePlain("timeline/fragments/newsletterResponse", w, params) 640} 641 642type KnotsParams struct { 643 BaseParams 644 Knots []KnotListingParams 645 Tab string 646} 647 648func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 649 params.Tab = "knots" 650 return p.execute("knots/index", w, params) 651} 652 653type KnotParams struct { 654 BaseParams 655 Registration *models.Registration 656 Members []string 657 Repos map[string][]models.Repo 658 IsOwner bool 659 RepoCount int 660 Tab string 661} 662 663func (p *Pages) Knot(w io.Writer, params KnotParams) error { 664 return p.execute("knots/dashboard", w, params) 665} 666 667type KnotListingParams struct { 668 *models.Registration 669 RepoCount int 670} 671 672func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 673 return p.executePlain("knots/fragments/knotListing", w, params) 674} 675 676type SpindlesParams struct { 677 BaseParams 678 Spindles []models.Spindle 679 Tab string 680} 681 682func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 683 params.Tab = "spindles" 684 return p.execute("spindles/index", w, params) 685} 686 687type SpindleListingParams struct { 688 models.Spindle 689 Tab string 690} 691 692func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 693 return p.executePlain("spindles/fragments/spindleListing", w, params) 694} 695 696type SpindleDashboardParams struct { 697 BaseParams 698 Spindle models.Spindle 699 Members []string 700 Repos map[string][]models.Repo 701 Tab string 702} 703 704func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 705 return p.execute("spindles/dashboard", w, params) 706} 707 708type NewRepoParams struct { 709 BaseParams 710 Knots []string 711 Spindles []string 712} 713 714func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 715 return p.execute("repo/new", w, params) 716} 717 718type ForkRepoParams struct { 719 BaseParams 720 Knots []string 721 Spindles []string 722 RepoInfo repoinfo.RepoInfo 723} 724 725func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 726 return p.execute("repo/fork", w, params) 727} 728 729type ProfileCard struct { 730 UserDid string 731 HasProfile bool 732 IsTangledUser bool 733 FollowStatus models.FollowStatus 734 VouchRelationship *models.VouchRelationship 735 Punchcard *models.Punchcard 736 Profile *models.Profile 737 Stats ProfileStats 738 Active string 739 ProfileScript string 740} 741 742type ProfileStats struct { 743 RepoCount int64 744 StarredCount int64 745 StringCount int64 746 FollowersCount int64 747 FollowingCount int64 748} 749 750func (p *ProfileCard) GetTabs() [][]any { 751 tabs := [][]any{ 752 {"overview", "overview", "square-chart-gantt", nil}, 753 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 754 {"starred", "starred", "star", p.Stats.StarredCount}, 755 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 756 {"vouches", "vouches", "shield", nil}, 757 } 758 759 return tabs 760} 761 762type ProfileOverviewParams struct { 763 BaseParams 764 Repos []models.Repo 765 CollaboratingRepos []models.Repo 766 ProfileTimeline *models.ProfileTimeline 767 Card *ProfileCard 768 Active string 769 ShowPunchcard bool 770} 771 772func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 773 params.Active = "overview" 774 return p.executeProfile("user/overview", w, params) 775} 776 777type ProfileReposParams struct { 778 BaseParams 779 Repos []models.Repo 780 StarStatuses map[string]bool 781 Card *ProfileCard 782 Active string 783 Page pagination.Page 784 RepoCount int 785 FilterQuery string 786} 787 788func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 789 params.Active = "repos" 790 return p.executeProfile("user/repos", w, params) 791} 792 793type ProfileStarredParams struct { 794 BaseParams 795 Repos []models.Repo 796 Card *ProfileCard 797 Page pagination.Page 798 Total int 799 Active string 800} 801 802func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 803 params.Active = "starred" 804 return p.executeProfile("user/starred", w, params) 805} 806 807type ProfileStringsParams struct { 808 BaseParams 809 Strings []models.String 810 Card *ProfileCard 811 Active string 812} 813 814func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 815 params.Active = "strings" 816 return p.executeProfile("user/strings", w, params) 817} 818 819type ProfileVouchesParams struct { 820 BaseParams 821 Vouches []models.Vouch 822 Suggestions []models.VouchSuggestion 823 Card *ProfileCard 824 Page pagination.Page 825 VouchCount int 826 Active string 827 EvidencePulls map[syntax.ATURI]*models.Pull 828 EvidenceIssues map[syntax.ATURI]*models.Issue 829} 830 831func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 832 params.Active = "vouches" 833 return p.executeProfile("user/vouches", w, params) 834} 835 836type FollowCard struct { 837 UserDid string 838 BaseParams 839 FollowStatus models.FollowStatus 840 FollowersCount int64 841 FollowingCount int64 842 Profile *models.Profile 843} 844 845type ProfileFollowersParams struct { 846 BaseParams 847 Followers []FollowCard 848 Card *ProfileCard 849 Active string 850} 851 852func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 853 params.Active = "overview" 854 return p.executeProfile("user/followers", w, params) 855} 856 857type ProfileFollowingParams struct { 858 BaseParams 859 Following []FollowCard 860 Card *ProfileCard 861 Active string 862} 863 864func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 865 params.Active = "overview" 866 return p.executeProfile("user/following", w, params) 867} 868 869type FollowFragmentParams struct { 870 UserDid string 871 FollowStatus models.FollowStatus 872 FollowersCount int64 873 HxSwapOob struct{} // empty struct so always truthy 874} 875 876func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 877 return p.executePlain("user/fragments/follow-oob", w, params) 878} 879 880type ProfilePopoverParams struct { 881 BaseParams 882 UserDid string 883 Profile *models.Profile 884 FollowStatus models.FollowStatus 885 VouchRelationship *models.VouchRelationship 886 Stats ProfilePopoverStats 887} 888 889type ProfilePopoverStats struct { 890 FollowersCount int64 891 FollowingCount int64 892} 893 894func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 895 return p.executePlain("user/fragments/profilePopover", w, params) 896} 897 898type EditBioParams struct { 899 BaseParams 900 Profile *models.Profile 901 AlsoKnownAs []string 902 // Action optionally overrides the form's hx-post target. Defaults to 903 // /profile/bio when empty. Used by the onboarding flow to save + advance. 904 Action string 905} 906 907func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 908 return p.executePlain("user/fragments/editBio", w, params) 909} 910 911type EditPinsParams struct { 912 BaseParams 913 Profile *models.Profile 914 AllRepos []PinnedRepo 915} 916 917type PinnedRepo struct { 918 IsPinned bool 919 models.Repo 920} 921 922func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 923 return p.executePlain("user/fragments/editPins", w, params) 924} 925 926type StarBtnFragmentParams struct { 927 IsStarred bool 928 SubjectAt syntax.ATURI 929 StarCount int 930 RepoName string 931 HxSwapOob bool 932} 933 934func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 935 params.HxSwapOob = true 936 return p.executePlain("fragments/starBtn", w, params) 937} 938 939type OnboardingParams struct { 940 BaseParams 941 Step int 942 943 EditBio EditBioParams 944 945 PubKeys []models.PublicKey 946 947 People []FollowCard 948 TrendingRepos []models.Repo 949 StarStatuses map[string]bool 950} 951 952// KeyFragment renders a single SSH key row (used to append a newly added key 953// to the list without a full reload). 954func (p *Pages) KeyFragment(w io.Writer, key models.PublicKey) error { 955 return p.executePlain("user/settings/fragments/keyListing", w, key) 956} 957 958func (p *Pages) Onboarding(w io.Writer, params OnboardingParams) error { 959 return p.executeOnboarding("onboarding/welcome", w, params) 960} 961 962type RepoIndexParams struct { 963 BaseParams 964 RepoInfo repoinfo.RepoInfo 965 Active string 966 TagMap map[string][]string 967 CommitsTrunc []types.Commit 968 TagsTrunc []*types.TagReference 969 BranchesTrunc []types.Branch 970 // ForkInfo *types.ForkInfo 971 HTMLReadme template.HTML 972 Raw bool 973 EmailToDid map[string]string 974 VerifiedCommits commitverify.VerifiedCommits 975 Languages []types.RepoLanguageDetails 976 NeedsKnotUpgrade bool 977 KnotUnreachable bool 978 types.RepoIndexResponse 979} 980 981func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 982 params.Active = "overview" 983 if params.IsEmpty { 984 return p.executeRepo("repo/empty", w, params) 985 } 986 987 if params.NeedsKnotUpgrade { 988 return p.executeRepo("repo/needsUpgrade", w, params) 989 } 990 991 if params.KnotUnreachable { 992 return p.executeRepo("repo/knotUnreachable", w, params) 993 } 994 995 rctx := p.rctx.Clone() 996 rctx.RepoInfo = params.RepoInfo 997 rctx.RepoInfo.Ref = params.Ref 998 rctx.RendererType = markup.RendererTypeRepoMarkdown 999 1000 if params.ReadmeFileName != "" { 1001 switch markup.GetFormat(params.ReadmeFileName) { 1002 case markup.FormatMarkdown: 1003 params.Raw = false 1004 htmlString := rctx.RenderMarkdown(params.Readme) 1005 sanitized := sanitizer.SanitizeDefault(htmlString) 1006 params.HTMLReadme = template.HTML(sanitized) 1007 default: 1008 params.Raw = true 1009 } 1010 } 1011 1012 return p.executeRepo("repo/index", w, params) 1013} 1014 1015type RepoSearchParams struct { 1016 BaseParams 1017 RepoInfo repoinfo.RepoInfo 1018 Active string 1019 FilterQuery string 1020} 1021 1022func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 1023 params.Active = "overview" 1024 return p.executeRepo("repo/search", w, params) 1025} 1026 1027type RepoSearchResultsFragmentParams struct { 1028 Query string 1029 Results []SearchResult 1030 ErrorMsg string 1031} 1032 1033func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 1034 return p.executePlain("repo/fragments/searchResults", w, params) 1035} 1036 1037type RepoLogParams struct { 1038 BaseParams 1039 RepoInfo repoinfo.RepoInfo 1040 TagMap map[string][]string 1041 Active string 1042 EmailToDid map[string]string 1043 VerifiedCommits commitverify.VerifiedCommits 1044 1045 types.RepoLogResponse 1046} 1047 1048func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 1049 params.Active = "overview" 1050 return p.executeRepo("repo/log", w, params) 1051} 1052 1053type RepoCommitParams struct { 1054 BaseParams 1055 RepoInfo repoinfo.RepoInfo 1056 Active string 1057 EmailToDid map[string]string 1058 Pipeline *types.Pipeline 1059 DiffOpts types.DiffOpts 1060 1061 // singular because it's always going to be just one 1062 VerifiedCommit commitverify.VerifiedCommits 1063 1064 types.RepoCommitResponse 1065} 1066 1067func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 1068 params.Active = "overview" 1069 return p.executeRepo("repo/commit", w, params) 1070} 1071 1072type RepoTreeParams struct { 1073 BaseParams 1074 RepoInfo repoinfo.RepoInfo 1075 Active string 1076 BreadCrumbs [][]string 1077 Path string 1078 Raw bool 1079 HTMLReadme template.HTML 1080 EmailToDid map[string]string 1081 LastCommitInfo *types.LastCommitInfo 1082 Ref string 1083 Parent string 1084 DotDot string 1085 Files []types.NiceTree 1086 ReadmeFileName string 1087 Readme string 1088} 1089 1090type RepoTreeStats struct { 1091 NumFolders uint64 1092 NumFiles uint64 1093} 1094 1095func (r RepoTreeParams) TreeStats() RepoTreeStats { 1096 numFolders, numFiles := 0, 0 1097 for _, f := range r.Files { 1098 if !f.IsFile() { 1099 numFolders += 1 1100 } else if f.IsFile() { 1101 numFiles += 1 1102 } 1103 } 1104 1105 return RepoTreeStats{ 1106 NumFolders: uint64(numFolders), 1107 NumFiles: uint64(numFiles), 1108 } 1109} 1110 1111func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 1112 params.Active = "overview" 1113 1114 rctx := p.rctx.Clone() 1115 rctx.RepoInfo = params.RepoInfo 1116 rctx.RepoInfo.Ref = params.Ref 1117 rctx.RendererType = markup.RendererTypeRepoMarkdown 1118 1119 if params.ReadmeFileName != "" { 1120 switch markup.GetFormat(params.ReadmeFileName) { 1121 case markup.FormatMarkdown: 1122 params.Raw = false 1123 htmlString := rctx.RenderMarkdown(params.Readme) 1124 sanitized := sanitizer.SanitizeDefault(htmlString) 1125 params.HTMLReadme = template.HTML(sanitized) 1126 default: 1127 params.Raw = true 1128 } 1129 } 1130 1131 return p.executeRepo("repo/tree", w, params) 1132} 1133 1134type RepoBranchesParams struct { 1135 BaseParams 1136 RepoInfo repoinfo.RepoInfo 1137 Active string 1138 types.RepoBranchesResponse 1139} 1140 1141func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 1142 params.Active = "overview" 1143 return p.executeRepo("repo/branches", w, params) 1144} 1145 1146type RepoTagsParams struct { 1147 BaseParams 1148 RepoInfo repoinfo.RepoInfo 1149 Active string 1150 types.RepoTagsResponse 1151 ArtifactMap map[plumbing.Hash][]models.Artifact 1152 DanglingArtifacts []models.Artifact 1153} 1154 1155func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1156 params.Active = "overview" 1157 return p.executeRepo("repo/tags", w, params) 1158} 1159 1160type RepoTagParams struct { 1161 BaseParams 1162 RepoInfo repoinfo.RepoInfo 1163 Active string 1164 types.RepoTagResponse 1165 ArtifactMap map[plumbing.Hash][]models.Artifact 1166 DanglingArtifacts []models.Artifact 1167} 1168 1169func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1170 params.Active = "overview" 1171 return p.executeRepo("repo/tag", w, params) 1172} 1173 1174type RepoArtifactParams struct { 1175 BaseParams 1176 RepoInfo repoinfo.RepoInfo 1177 Artifact models.Artifact 1178} 1179 1180func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1181 return p.executePlain("repo/fragments/artifact", w, params) 1182} 1183 1184type RepoBlobParams struct { 1185 BaseParams 1186 RepoInfo repoinfo.RepoInfo 1187 Active string // always "overview" 1188 BreadCrumbs [][]string 1189 BlobView models.BlobView // TODO: expose this struct 1190 ShowRendered bool 1191 EmailToDid map[string]string 1192 LastCommitInfo *types.LastCommitInfo 1193 Ref string 1194 Path string 1195 Language string 1196} 1197 1198func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1199 params.Active = "overview" 1200 return p.executeRepo("repo/blob", w, params) 1201} 1202 1203type Collaborator struct { 1204 Did string 1205 Role string 1206} 1207 1208type RepoSettingsParams struct { 1209 BaseParams 1210 RepoInfo repoinfo.RepoInfo 1211 Collaborators []Collaborator 1212 Active string 1213 Branches []types.Branch 1214 Spindles []string 1215 CurrentSpindle string 1216 Secrets []*tangled.RepoListSecrets_Secret 1217 1218 // TODO: use repoinfo.roles 1219 IsCollaboratorInviteAllowed bool 1220} 1221 1222func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1223 params.Active = "settings" 1224 return p.executeRepo("repo/settings", w, params) 1225} 1226 1227type RepoGeneralSettingsParams struct { 1228 BaseParams 1229 RepoInfo repoinfo.RepoInfo 1230 Labels []models.LabelDefinition 1231 DefaultLabels []models.LabelDefinition 1232 SubscribedLabels map[string]struct{} 1233 ShouldSubscribeAll bool 1234 Active string 1235 Tab string 1236 Branches []types.Branch 1237} 1238 1239func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1240 params.Active = "settings" 1241 params.Tab = "general" 1242 return p.executeRepo("repo/settings/general", w, params) 1243} 1244 1245type RepoAccessSettingsParams struct { 1246 BaseParams 1247 RepoInfo repoinfo.RepoInfo 1248 Active string 1249 Tab string 1250 Collaborators []Collaborator 1251 CanRemoveCollaborator bool 1252} 1253 1254func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1255 params.Active = "settings" 1256 params.Tab = "access" 1257 return p.executeRepo("repo/settings/access", w, params) 1258} 1259 1260type RepoPipelineSettingsParams struct { 1261 BaseParams 1262 RepoInfo repoinfo.RepoInfo 1263 Active string 1264 Tab string 1265 Spindles []string 1266 CurrentSpindle string 1267 Secrets []map[string]any 1268} 1269 1270func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1271 params.Active = "settings" 1272 params.Tab = "pipelines" 1273 return p.executeRepo("repo/settings/pipelines", w, params) 1274} 1275 1276type RepoWebhooksSettingsParams struct { 1277 BaseParams 1278 RepoInfo repoinfo.RepoInfo 1279 Active string 1280 Tab string 1281 Webhooks []models.Webhook 1282 WebhookDeliveries map[int64][]models.WebhookDelivery 1283} 1284 1285func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1286 params.Active = "settings" 1287 params.Tab = "hooks" 1288 return p.executeRepo("repo/settings/hooks", w, params) 1289} 1290 1291type WebhookDeliveriesListParams struct { 1292 BaseParams 1293 RepoInfo repoinfo.RepoInfo 1294 Webhook *models.Webhook 1295 Deliveries []models.WebhookDelivery 1296} 1297 1298func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1299 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1300 if err != nil { 1301 return err 1302 } 1303 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1304} 1305 1306type RepoSiteSettingsParams struct { 1307 BaseParams 1308 RepoInfo repoinfo.RepoInfo 1309 Active string 1310 Tab string 1311 Branches []types.Branch 1312 SiteConfig *models.RepoSite 1313 OwnerClaim *models.DomainClaim 1314 Deploys []models.SiteDeploy 1315 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1316} 1317 1318func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1319 params.Active = "settings" 1320 params.Tab = "sites" 1321 return p.executeRepo("repo/settings/sites", w, params) 1322} 1323 1324type RepoIssuesParams struct { 1325 BaseParams 1326 RepoInfo repoinfo.RepoInfo 1327 Active string 1328 Issues []models.Issue 1329 IssueCount int 1330 LabelDefs map[string]*models.LabelDefinition 1331 Page pagination.Page 1332 FilterState string 1333 FilterQuery string 1334 BaseFilterQuery string 1335 VouchRelationships map[syntax.DID]*models.VouchRelationship 1336} 1337 1338func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1339 params.Active = "issues" 1340 return p.executeRepo("repo/issues/issues", w, params) 1341} 1342 1343type RepoSingleIssueParams struct { 1344 BaseParams 1345 RepoInfo repoinfo.RepoInfo 1346 Active string 1347 Issue *models.Issue 1348 CommentList []models.CommentListItem 1349 Backlinks []models.RichReferenceLink 1350 LabelDefs map[string]*models.LabelDefinition 1351 1352 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1353 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1354 VouchRelationships map[syntax.DID]*models.VouchRelationship 1355 1356 // IsSubscribed is nil when the user is not logged in, true when subscribed, 1357 // false when explicitly unsubscribed, and nil when no explicit subscription. 1358 IsSubscribed *bool 1359} 1360 1361func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1362 params.Active = "issues" 1363 return p.executeRepo("repo/issues/issue", w, params) 1364} 1365 1366type IssueSubscribeParams struct { 1367 RepoInfo repoinfo.RepoInfo 1368 IssueId int 1369 IsSubscribed *bool 1370} 1371 1372func (p *Pages) IssueSubscribeFragment(w io.Writer, params IssueSubscribeParams) error { 1373 return p.executePlain("repo/issues/fragments/subscribeButton", w, params) 1374} 1375 1376type PullSubscribeParams struct { 1377 RepoInfo repoinfo.RepoInfo 1378 PullId int 1379 IsSubscribed *bool 1380} 1381 1382func (p *Pages) PullSubscribeFragment(w io.Writer, params PullSubscribeParams) error { 1383 return p.executePlain("repo/pulls/fragments/subscribeButton", w, params) 1384} 1385 1386type EditIssueParams struct { 1387 BaseParams 1388 RepoInfo repoinfo.RepoInfo 1389 Issue *models.Issue 1390 Action string 1391} 1392 1393func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1394 params.Action = "edit" 1395 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1396} 1397 1398type ThreadReactionFragmentParams struct { 1399 Kind models.ReactionKind 1400 Count int 1401 Users []string 1402 IsReacted bool 1403 CommentRkey string 1404 SubjectUri string 1405} 1406 1407func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1408 return p.executePlain("repo/fragments/reaction", w, params) 1409} 1410 1411type RepoNewIssueParams struct { 1412 BaseParams 1413 RepoInfo repoinfo.RepoInfo 1414 Issue *models.Issue // existing issue if any -- passed when editing 1415 Active string 1416 Action string 1417} 1418 1419func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1420 params.Active = "issues" 1421 params.Action = "create" 1422 return p.executeRepo("repo/issues/new", w, params) 1423} 1424 1425type StackedDiff struct { 1426 Diff *types.NiceDiff 1427 Opts types.DiffOpts 1428} 1429 1430type RepoNewPullParams struct { 1431 BaseParams 1432 RepoInfo repoinfo.RepoInfo 1433 Branches []types.Branch 1434 SourceBranches []types.Branch 1435 ForkBranches []types.Branch 1436 Forks []models.Repo 1437 Source Source 1438 SourceBranch string 1439 TargetBranch string 1440 Fork string 1441 Patch string 1442 Title string 1443 Body string 1444 TitleDirty bool 1445 BodyDirty bool 1446 IsStacked bool 1447 Comparison *types.RepoFormatPatchResponse 1448 Diff *types.NiceDiff 1449 DiffOpts types.DiffOpts 1450 StackedDiffs []StackedDiff 1451 MergeCheck *types.MergeCheckResponse 1452 StackTitles map[string]string 1453 StackBodies map[string]string 1454 PrefillError string 1455 Active string 1456 LabelDefs map[string]*models.LabelDefinition 1457 LabelState models.LabelState 1458 StackLabelStates map[string]models.LabelState 1459} 1460 1461func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1462 params.Active = "pulls" 1463 return p.executeRepo("repo/pulls/new", w, params) 1464} 1465 1466func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1467 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1468} 1469 1470func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1471 return p.executePlain("fragments/markdownPreview", w, body) 1472} 1473 1474type EditPullParams struct { 1475 LoggedInUser *oauth.MultiAccountUser 1476 RepoInfo repoinfo.RepoInfo 1477 Pull *models.Pull 1478} 1479 1480func (p *Pages) EditPullFragment(w io.Writer, params EditPullParams) error { 1481 return p.executePlain("repo/pulls/fragments/pullEdit", w, params) 1482} 1483 1484type RepoPullsParams struct { 1485 BaseParams 1486 RepoInfo repoinfo.RepoInfo 1487 Pulls []*models.Pull 1488 Active string 1489 FilterState string 1490 FilterQuery string 1491 BaseFilterQuery string 1492 Stacks []models.Stack 1493 Pipelines map[string]types.Pipeline 1494 LabelDefs map[string]*models.LabelDefinition 1495 Page pagination.Page 1496 PullCount int 1497 VouchRelationships map[syntax.DID]*models.VouchRelationship 1498} 1499 1500func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1501 params.Active = "pulls" 1502 return p.executeRepo("repo/pulls/pulls", w, params) 1503} 1504 1505type ResubmitResult uint64 1506 1507const ( 1508 ShouldResubmit ResubmitResult = iota 1509 ShouldNotResubmit 1510 Unknown 1511) 1512 1513func (r ResubmitResult) Yes() bool { 1514 return r == ShouldResubmit 1515} 1516func (r ResubmitResult) No() bool { 1517 return r == ShouldNotResubmit 1518} 1519func (r ResubmitResult) Unknown() bool { 1520 return r == Unknown 1521} 1522 1523type RepoSinglePullParams struct { 1524 BaseParams 1525 RepoInfo repoinfo.RepoInfo 1526 Active string 1527 Pull *models.Pull 1528 Stack models.Stack 1529 Backlinks []models.RichReferenceLink 1530 BranchDeleteStatus *models.BranchDeleteStatus 1531 MergeCheck types.MergeCheckResponse 1532 ResubmitCheck ResubmitResult 1533 Pipelines map[string]types.Pipeline 1534 Diff types.DiffRenderer 1535 DiffOpts types.DiffOpts 1536 ActiveRound int 1537 IsInterdiff bool 1538 1539 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1540 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1541 1542 LabelDefs map[string]*models.LabelDefinition 1543 VouchRelationships map[syntax.DID]*models.VouchRelationship 1544 VouchSkips map[syntax.DID]bool 1545 1546 // IsSubscribed is nil when not logged in, true when subscribed, false when explicitly unsubscribed. 1547 IsSubscribed *bool 1548} 1549 1550func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1551 params.Active = "pulls" 1552 return p.executeRepo("repo/pulls/pull", w, params) 1553} 1554 1555type PullResubmitParams struct { 1556 BaseParams 1557 RepoInfo repoinfo.RepoInfo 1558 Pull *models.Pull 1559 SubmissionId int 1560} 1561 1562func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1563 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1564} 1565 1566type PullActionsParams struct { 1567 BaseParams 1568 RepoInfo repoinfo.RepoInfo 1569 Pull *models.Pull 1570 RoundNumber int 1571 MergeCheck types.MergeCheckResponse 1572 ResubmitCheck ResubmitResult 1573 BranchDeleteStatus *models.BranchDeleteStatus 1574 Stack models.Stack 1575 1576 // Workflow warning state for fork-based pulls without a pipeline on the 1577 // latest commit. WorkflowsChanged and ChangedWorkflowFiles are computed 1578 // from the latest round's patch. 1579 WorkflowsChanged bool 1580 ChangedWorkflowFiles []string 1581 HasPipeline bool 1582 1583 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1584 // that fetches the real, checked fragment 1585 Loading bool 1586} 1587 1588func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1589 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1590} 1591 1592type RepoCompareParams struct { 1593 BaseParams 1594 RepoInfo repoinfo.RepoInfo 1595 Forks []models.Repo 1596 Branches []types.Branch 1597 Tags []*types.TagReference 1598 Base string 1599 Head string 1600 Diff *types.NiceDiff 1601 DiffOpts types.DiffOpts 1602 1603 Active string 1604} 1605 1606func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1607 params.Active = "overview" 1608 return p.executeRepo("repo/compare/compare", w, params) 1609} 1610 1611type RepoCompareNewParams struct { 1612 BaseParams 1613 RepoInfo repoinfo.RepoInfo 1614 Forks []models.Repo 1615 Branches []types.Branch 1616 Tags []*types.TagReference 1617 Base string 1618 Head string 1619 1620 Active string 1621} 1622 1623func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1624 params.Active = "overview" 1625 return p.executeRepo("repo/compare/new", w, params) 1626} 1627 1628type RepoCompareAllowPullParams struct { 1629 BaseParams 1630 RepoInfo repoinfo.RepoInfo 1631 Base string 1632 Head string 1633} 1634 1635func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1636 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1637} 1638 1639type RepoCompareDiffFragmentParams struct { 1640 Diff types.NiceDiff 1641 DiffOpts types.DiffOpts 1642} 1643 1644func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1645 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1646} 1647 1648type LabelPanelParams struct { 1649 BaseParams 1650 RepoInfo repoinfo.RepoInfo 1651 Defs map[string]*models.LabelDefinition 1652 Subject string 1653 State models.LabelState 1654} 1655 1656func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1657 return p.executePlain("repo/fragments/labelPanel", w, params) 1658} 1659 1660type EditLabelPanelParams struct { 1661 BaseParams 1662 RepoInfo repoinfo.RepoInfo 1663 Defs map[string]*models.LabelDefinition 1664 Subject string 1665 State models.LabelState 1666 Prefix string 1667} 1668 1669func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1670 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1671} 1672 1673type RepoStarsParams struct { 1674 BaseParams 1675 RepoInfo repoinfo.RepoInfo 1676 Active string 1677 Starrers []models.Star 1678 Page pagination.Page 1679 TotalCount int 1680} 1681 1682func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1683 params.Active = "overview" 1684 return p.executeRepo("repo/stars", w, params) 1685} 1686 1687type RepoForksParams struct { 1688 BaseParams 1689 RepoInfo repoinfo.RepoInfo 1690 Active string 1691 Forks []models.Repo 1692 Page pagination.Page 1693 TotalCount int 1694} 1695 1696func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1697 params.Active = "overview" 1698 return p.executeRepo("repo/forks", w, params) 1699} 1700 1701type PipelinesParams struct { 1702 BaseParams 1703 RepoInfo repoinfo.RepoInfo 1704 Pipelines []types.Pipeline 1705 Active string 1706 FilterKind string 1707 Total int64 1708} 1709 1710func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1711 params.Active = "pipelines" 1712 return p.executeRepo("repo/pipelines/pipelines", w, params) 1713} 1714 1715type LogBlockParams struct { 1716 Id int 1717 Name string 1718 Command string 1719 Collapsed bool 1720 StartTime time.Time 1721} 1722 1723func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1724 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1725} 1726 1727type LogBlockEndParams struct { 1728 Id int 1729 StartTime time.Time 1730 EndTime time.Time 1731} 1732 1733func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1734 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1735} 1736 1737type LogLineParams struct { 1738 Id int 1739 Content template.HTML 1740} 1741 1742func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1743 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1744} 1745 1746type WorkflowSymbolOOBParams struct { 1747 Name string 1748 Statuses models.WorkflowStatus 1749} 1750 1751func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1752 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1753} 1754 1755type PipelineStatusesParams struct { 1756 RepoInfo repoinfo.RepoInfo 1757 Pipelines map[string]types.Pipeline 1758} 1759 1760func (p *Pages) PipelineStatusesFragment(w io.Writer, params PipelineStatusesParams) error { 1761 return p.executePlain("repo/fragments/commitPipelineStatuses", w, params) 1762} 1763 1764type WorkflowParams struct { 1765 BaseParams 1766 RepoInfo repoinfo.RepoInfo 1767 Pipeline types.Pipeline 1768 Workflow string 1769 SSHLogCommand string 1770 Active string 1771} 1772 1773func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1774 params.Active = "pipelines" 1775 return p.executeRepo("repo/pipelines/workflow", w, params) 1776} 1777 1778type PutStringParams struct { 1779 BaseParams 1780 Action string 1781 1782 // this is supplied in the case of editing an existing string 1783 String models.String 1784} 1785 1786func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1787 return p.execute("strings/put", w, params) 1788} 1789 1790type StringsDashboardParams struct { 1791 BaseParams 1792 Card ProfileCard 1793 Strings []models.String 1794} 1795 1796func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1797 return p.execute("strings/dashboard", w, params) 1798} 1799 1800type StringTimelineParams struct { 1801 BaseParams 1802 Strings []models.String 1803} 1804 1805func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1806 return p.execute("strings/timeline", w, params) 1807} 1808 1809type SingleStringParams struct { 1810 BaseParams 1811 ShowRendered bool 1812 RenderToggle bool 1813 RenderedContents template.HTML 1814 String *models.String 1815 Stats models.StringStats 1816 IsStarred bool 1817 StarCount int 1818 Owner identity.Identity 1819 CommentList []models.CommentListItem 1820 1821 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1822 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1823 VouchRelationships map[syntax.DID]*models.VouchRelationship 1824} 1825 1826func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1827 return p.execute("strings/string", w, params) 1828} 1829 1830type SearchReposParams struct { 1831 BaseParams 1832 FilterType string // "repo" | "code" 1833 Repos []SearchResult 1834 Page pagination.Page 1835 ResultCount int 1836 FilterQuery string 1837 SortParam string 1838 TimeTaken time.Duration 1839 DocCount int64 1840 ErrorMsg string 1841} 1842 1843func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1844 params.FilterType = "repo" 1845 return p.execute("search/search", w, params) 1846} 1847 1848type SearchQuickParams struct { 1849 Repos []models.Repo 1850 Query string 1851 Total int 1852} 1853 1854func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1855 return p.executePlain("search/fragments/quick", w, params) 1856} 1857 1858func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1859 tpl, err := p.parse("search/fragments/quick") 1860 if err != nil { 1861 return err 1862 } 1863 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1864} 1865 1866type SearchResult struct { 1867 RepoDID syntax.DID 1868 Repo *models.Repo 1869 FilePath string 1870 Branches []string 1871 Commit string 1872 Language string 1873 1874 File *CodeSearchResult_File // filename match 1875 Chunks CodeSearchResult_Chunks // content matches 1876} 1877 1878// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1879type CodeSearchResult_Chunk struct { 1880 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1881 MatchCount int // number of match ranges in this chunk 1882} 1883 1884type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1885 1886func (cs CodeSearchResult_Chunks) MatchCount() int { 1887 count := 0 1888 for _, c := range cs { 1889 count += c.MatchCount 1890 } 1891 return count 1892} 1893 1894type CodeSearchResult_File struct { 1895 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1896} 1897 1898type ChunkSpan struct { 1899 Text string 1900 Match bool 1901} 1902 1903type ChunkLine struct { 1904 Num int 1905 Spans []ChunkSpan 1906 Highlight bool 1907} 1908 1909// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1910// line into matched/unmatched spans using ranges. startLine is the 1-based line 1911// number of the first line. 1912func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1913 if startLine < 1 { 1914 startLine = 1 1915 } 1916 // trim a single trailing newline so we don't emit a spurious empty line 1917 content = strings.TrimSuffix(content, "\n") 1918 lines := strings.Split(content, "\n") 1919 out := make([]ChunkLine, len(lines)) 1920 for i, text := range lines { 1921 num := startLine + i 1922 runes := []rune(text) 1923 1924 // collect matched rune intervals [c0,c1) for this line 1925 var intervals [][2]int 1926 for _, rg := range ranges { 1927 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1928 continue 1929 } 1930 c0, c1 := 0, len(runes) 1931 if num == int(rg.Start.LineNumber) { 1932 c0 = int(rg.Start.Column) - 1 1933 } 1934 if num == int(rg.End.LineNumber) { 1935 c1 = int(rg.End.Column) - 1 1936 } 1937 c0 = max(0, min(c0, len(runes))) 1938 c1 = max(0, min(c1, len(runes))) 1939 if c0 < c1 { 1940 intervals = append(intervals, [2]int{c0, c1}) 1941 } 1942 } 1943 intervals = mergeIntervals(intervals) 1944 1945 out[i] = ChunkLine{ 1946 Num: num, 1947 Spans: spanRunes(runes, intervals), 1948 Highlight: len(intervals) > 0, 1949 } 1950 } 1951 return out 1952} 1953 1954// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1955// Filename ranges live on line 1; columns are clamped to rune bounds. 1956func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1957 runes := []rune(name) 1958 var intervals [][2]int 1959 for _, rg := range ranges { 1960 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1961 continue 1962 } 1963 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1964 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1965 if c0 < c1 { 1966 intervals = append(intervals, [2]int{c0, c1}) 1967 } 1968 } 1969 return spanRunes(runes, mergeIntervals(intervals)) 1970} 1971 1972type CodeSearchParams struct { 1973 BaseParams 1974 FilterType string // "code" 1975 FilterQuery string 1976 Results []SearchResult 1977 Page pagination.Page 1978 HasMore bool 1979 ErrorMsg string 1980 1981 MatchCount int 1982 FileCount int 1983 TimeTaken time.Duration 1984} 1985 1986func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1987 params.FilterType = "code" 1988 return p.execute("search/search", w, params) 1989} 1990 1991func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1992 return p.execute("timeline/home", w, params) 1993} 1994 1995type CommentBodyFragmentParams struct { 1996 Comment models.Comment 1997 Reactions map[models.ReactionKind]models.ReactionDisplayData 1998 UserReacted map[models.ReactionKind]bool 1999} 2000 2001func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 2002 return p.executePlain("fragments/comment/commentBody", w, params) 2003} 2004 2005type PullCommentFragmentParams struct { 2006 LoggedInUser *oauth.MultiAccountUser 2007 Comment models.Comment 2008 Reactions map[models.ReactionKind]models.ReactionDisplayData 2009 UserReacted map[models.ReactionKind]bool 2010 HxSwapOob bool 2011} 2012 2013func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error { 2014 return p.executePlain("fragments/comment/pullComment", w, params) 2015} 2016 2017type CommentHeaderFragmentParams struct { 2018 Comment models.Comment 2019 Reactions map[models.ReactionKind]models.ReactionDisplayData 2020 UserReacted map[models.ReactionKind]bool 2021 HxSwapOob bool 2022} 2023 2024func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 2025 return p.executePlain("fragments/comment/commentHeader", w, params) 2026} 2027 2028type EditCommentFragmentParams struct { 2029 Comment models.Comment 2030} 2031 2032func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 2033 return p.executePlain("fragments/comment/edit", w, params) 2034} 2035 2036type ReplyCommentFragmentParams struct { 2037 BaseParams 2038} 2039 2040func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 2041 return p.executePlain("fragments/comment/reply", w, params) 2042} 2043 2044type ReplyPlaceholderFragmentParams struct { 2045 BaseParams 2046} 2047 2048func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 2049 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 2050} 2051 2052func (p *Pages) Static() http.Handler { 2053 if p.dev { 2054 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 2055 } 2056 2057 sub, err := fs.Sub(p.embedFS, "static") 2058 if err != nil { 2059 p.logger.Error("no static dir found? that's crazy", "err", err) 2060 panic(err) 2061 } 2062 // Custom handler to apply Cache-Control headers for font files 2063 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 2064} 2065 2066func Cache(h http.Handler) http.Handler { 2067 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2068 path := strings.Split(r.URL.Path, "?")[0] 2069 2070 if strings.HasSuffix(path, ".css") { 2071 // on day for css files 2072 w.Header().Set("Cache-Control", "public, max-age=86400") 2073 } else { 2074 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 2075 } 2076 h.ServeHTTP(w, r) 2077 }) 2078} 2079 2080func (p *Pages) CssContentHash() string { 2081 cssFile, err := p.embedFS.Open("static/tw.css") 2082 if err != nil { 2083 slog.Debug("Error opening CSS file", "err", err) 2084 return "" 2085 } 2086 defer cssFile.Close() 2087 2088 hasher := sha256.New() 2089 if _, err := io.Copy(hasher, cssFile); err != nil { 2090 slog.Debug("Error hashing CSS file", "err", err) 2091 return "" 2092 } 2093 2094 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 2095} 2096 2097func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 2098 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 2099} 2100 2101func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 2102 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 2103} 2104 2105func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 2106 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 2107} 2108 2109func (p *Pages) Error500(w io.Writer) error { 2110 return p.execute("errors/500", w, BaseParams{}) 2111} 2112 2113func (p *Pages) Error404(w io.Writer) error { 2114 return p.execute("errors/404", w, BaseParams{}) 2115} 2116 2117func (p *Pages) ErrorKnot404(w io.Writer) error { 2118 return p.execute("errors/knot404", w, BaseParams{}) 2119} 2120 2121func (p *Pages) Error503(w io.Writer) error { 2122 return p.execute("errors/503", w, BaseParams{}) 2123}