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