This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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