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