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