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