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