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 2073 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 Pipelines map[string]types.Pipeline 961 NeedsKnotUpgrade bool 962 KnotUnreachable bool 963 types.RepoIndexResponse 964} 965 966func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 967 params.Active = "overview" 968 if params.IsEmpty { 969 return p.executeRepo("repo/empty", w, params) 970 } 971 972 if params.NeedsKnotUpgrade { 973 return p.executeRepo("repo/needsUpgrade", w, params) 974 } 975 976 if params.KnotUnreachable { 977 return p.executeRepo("repo/knotUnreachable", w, params) 978 } 979 980 rctx := p.rctx.Clone() 981 rctx.RepoInfo = params.RepoInfo 982 rctx.RepoInfo.Ref = params.Ref 983 rctx.RendererType = markup.RendererTypeRepoMarkdown 984 985 if params.ReadmeFileName != "" { 986 switch markup.GetFormat(params.ReadmeFileName) { 987 case markup.FormatMarkdown: 988 params.Raw = false 989 htmlString := rctx.RenderMarkdown(params.Readme) 990 sanitized := sanitizer.SanitizeDefault(htmlString) 991 params.HTMLReadme = template.HTML(sanitized) 992 default: 993 params.Raw = true 994 } 995 } 996 997 return p.executeRepo("repo/index", w, params) 998} 999 1000type RepoSearchParams struct { 1001 BaseParams 1002 RepoInfo repoinfo.RepoInfo 1003 Active string 1004 FilterQuery string 1005} 1006 1007func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 1008 params.Active = "overview" 1009 return p.executeRepo("repo/search", w, params) 1010} 1011 1012type RepoSearchResultsFragmentParams struct { 1013 Query string 1014 Results []SearchResult 1015 ErrorMsg string 1016} 1017 1018func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 1019 return p.executePlain("repo/fragments/searchResults", w, params) 1020} 1021 1022type RepoLogParams struct { 1023 BaseParams 1024 RepoInfo repoinfo.RepoInfo 1025 TagMap map[string][]string 1026 Active string 1027 EmailToDid map[string]string 1028 VerifiedCommits commitverify.VerifiedCommits 1029 Pipelines map[string]types.Pipeline 1030 1031 types.RepoLogResponse 1032} 1033 1034func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 1035 params.Active = "overview" 1036 return p.executeRepo("repo/log", w, params) 1037} 1038 1039type RepoCommitParams struct { 1040 BaseParams 1041 RepoInfo repoinfo.RepoInfo 1042 Active string 1043 EmailToDid map[string]string 1044 Pipeline *types.Pipeline 1045 DiffOpts types.DiffOpts 1046 1047 // singular because it's always going to be just one 1048 VerifiedCommit commitverify.VerifiedCommits 1049 1050 types.RepoCommitResponse 1051} 1052 1053func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 1054 params.Active = "overview" 1055 return p.executeRepo("repo/commit", w, params) 1056} 1057 1058type RepoTreeParams struct { 1059 BaseParams 1060 RepoInfo repoinfo.RepoInfo 1061 Active string 1062 BreadCrumbs [][]string 1063 Path string 1064 Raw bool 1065 HTMLReadme template.HTML 1066 EmailToDid map[string]string 1067 LastCommitInfo *types.LastCommitInfo 1068 Ref string 1069 Parent string 1070 DotDot string 1071 Files []types.NiceTree 1072 ReadmeFileName string 1073 Readme string 1074} 1075 1076type RepoTreeStats struct { 1077 NumFolders uint64 1078 NumFiles uint64 1079} 1080 1081func (r RepoTreeParams) TreeStats() RepoTreeStats { 1082 numFolders, numFiles := 0, 0 1083 for _, f := range r.Files { 1084 if !f.IsFile() { 1085 numFolders += 1 1086 } else if f.IsFile() { 1087 numFiles += 1 1088 } 1089 } 1090 1091 return RepoTreeStats{ 1092 NumFolders: uint64(numFolders), 1093 NumFiles: uint64(numFiles), 1094 } 1095} 1096 1097func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 1098 params.Active = "overview" 1099 1100 rctx := p.rctx.Clone() 1101 rctx.RepoInfo = params.RepoInfo 1102 rctx.RepoInfo.Ref = params.Ref 1103 rctx.RendererType = markup.RendererTypeRepoMarkdown 1104 1105 if params.ReadmeFileName != "" { 1106 switch markup.GetFormat(params.ReadmeFileName) { 1107 case markup.FormatMarkdown: 1108 params.Raw = false 1109 htmlString := rctx.RenderMarkdown(params.Readme) 1110 sanitized := sanitizer.SanitizeDefault(htmlString) 1111 params.HTMLReadme = template.HTML(sanitized) 1112 default: 1113 params.Raw = true 1114 } 1115 } 1116 1117 return p.executeRepo("repo/tree", w, params) 1118} 1119 1120type RepoBranchesParams struct { 1121 BaseParams 1122 RepoInfo repoinfo.RepoInfo 1123 Active string 1124 types.RepoBranchesResponse 1125} 1126 1127func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 1128 params.Active = "overview" 1129 return p.executeRepo("repo/branches", w, params) 1130} 1131 1132type RepoTagsParams struct { 1133 BaseParams 1134 RepoInfo repoinfo.RepoInfo 1135 Active string 1136 types.RepoTagsResponse 1137 ArtifactMap map[plumbing.Hash][]models.Artifact 1138 DanglingArtifacts []models.Artifact 1139} 1140 1141func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1142 params.Active = "overview" 1143 return p.executeRepo("repo/tags", w, params) 1144} 1145 1146type RepoTagParams struct { 1147 BaseParams 1148 RepoInfo repoinfo.RepoInfo 1149 Active string 1150 types.RepoTagResponse 1151 ArtifactMap map[plumbing.Hash][]models.Artifact 1152 DanglingArtifacts []models.Artifact 1153} 1154 1155func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1156 params.Active = "overview" 1157 return p.executeRepo("repo/tag", w, params) 1158} 1159 1160type RepoArtifactParams struct { 1161 BaseParams 1162 RepoInfo repoinfo.RepoInfo 1163 Artifact models.Artifact 1164} 1165 1166func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1167 return p.executePlain("repo/fragments/artifact", w, params) 1168} 1169 1170type RepoBlobParams struct { 1171 BaseParams 1172 RepoInfo repoinfo.RepoInfo 1173 Active string // always "overview" 1174 BreadCrumbs [][]string 1175 BlobView models.BlobView // TODO: expose this struct 1176 ShowRendered bool 1177 EmailToDid map[string]string 1178 LastCommitInfo *types.LastCommitInfo 1179 Ref string 1180 Path string 1181 Language string 1182} 1183 1184func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1185 params.Active = "overview" 1186 return p.executeRepo("repo/blob", w, params) 1187} 1188 1189type Collaborator struct { 1190 Did string 1191 Role string 1192} 1193 1194type RepoSettingsParams struct { 1195 BaseParams 1196 RepoInfo repoinfo.RepoInfo 1197 Collaborators []Collaborator 1198 Active string 1199 Branches []types.Branch 1200 Spindles []string 1201 CurrentSpindle string 1202 Secrets []*tangled.RepoListSecrets_Secret 1203 1204 // TODO: use repoinfo.roles 1205 IsCollaboratorInviteAllowed bool 1206} 1207 1208func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1209 params.Active = "settings" 1210 return p.executeRepo("repo/settings", w, params) 1211} 1212 1213type RepoGeneralSettingsParams struct { 1214 BaseParams 1215 RepoInfo repoinfo.RepoInfo 1216 Labels []models.LabelDefinition 1217 DefaultLabels []models.LabelDefinition 1218 SubscribedLabels map[string]struct{} 1219 ShouldSubscribeAll bool 1220 Active string 1221 Tab string 1222 Branches []types.Branch 1223} 1224 1225func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1226 params.Active = "settings" 1227 params.Tab = "general" 1228 return p.executeRepo("repo/settings/general", w, params) 1229} 1230 1231type RepoAccessSettingsParams struct { 1232 BaseParams 1233 RepoInfo repoinfo.RepoInfo 1234 Active string 1235 Tab string 1236 Collaborators []Collaborator 1237 CanRemoveCollaborator bool 1238} 1239 1240func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1241 params.Active = "settings" 1242 params.Tab = "access" 1243 return p.executeRepo("repo/settings/access", w, params) 1244} 1245 1246type RepoPipelineSettingsParams struct { 1247 BaseParams 1248 RepoInfo repoinfo.RepoInfo 1249 Active string 1250 Tab string 1251 Spindles []string 1252 CurrentSpindle string 1253 Secrets []map[string]any 1254} 1255 1256func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1257 params.Active = "settings" 1258 params.Tab = "pipelines" 1259 return p.executeRepo("repo/settings/pipelines", w, params) 1260} 1261 1262type RepoWebhooksSettingsParams struct { 1263 BaseParams 1264 RepoInfo repoinfo.RepoInfo 1265 Active string 1266 Tab string 1267 Webhooks []models.Webhook 1268 WebhookDeliveries map[int64][]models.WebhookDelivery 1269} 1270 1271func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1272 params.Active = "settings" 1273 params.Tab = "hooks" 1274 return p.executeRepo("repo/settings/hooks", w, params) 1275} 1276 1277type WebhookDeliveriesListParams struct { 1278 BaseParams 1279 RepoInfo repoinfo.RepoInfo 1280 Webhook *models.Webhook 1281 Deliveries []models.WebhookDelivery 1282} 1283 1284func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1285 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1286 if err != nil { 1287 return err 1288 } 1289 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1290} 1291 1292type RepoSiteSettingsParams struct { 1293 BaseParams 1294 RepoInfo repoinfo.RepoInfo 1295 Active string 1296 Tab string 1297 Branches []types.Branch 1298 SiteConfig *models.RepoSite 1299 OwnerClaim *models.DomainClaim 1300 Deploys []models.SiteDeploy 1301 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1302} 1303 1304func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1305 params.Active = "settings" 1306 params.Tab = "sites" 1307 return p.executeRepo("repo/settings/sites", w, params) 1308} 1309 1310type RepoIssuesParams struct { 1311 BaseParams 1312 RepoInfo repoinfo.RepoInfo 1313 Active string 1314 Issues []models.Issue 1315 IssueCount int 1316 LabelDefs map[string]*models.LabelDefinition 1317 Page pagination.Page 1318 FilterState string 1319 FilterQuery string 1320 BaseFilterQuery string 1321 VouchRelationships map[syntax.DID]*models.VouchRelationship 1322} 1323 1324func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1325 params.Active = "issues" 1326 return p.executeRepo("repo/issues/issues", w, params) 1327} 1328 1329type RepoSingleIssueParams struct { 1330 BaseParams 1331 RepoInfo repoinfo.RepoInfo 1332 Active string 1333 Issue *models.Issue 1334 CommentList []models.CommentListItem 1335 Backlinks []models.RichReferenceLink 1336 LabelDefs map[string]*models.LabelDefinition 1337 1338 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1339 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1340 VouchRelationships map[syntax.DID]*models.VouchRelationship 1341} 1342 1343func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1344 params.Active = "issues" 1345 return p.executeRepo("repo/issues/issue", w, params) 1346} 1347 1348type EditIssueParams struct { 1349 BaseParams 1350 RepoInfo repoinfo.RepoInfo 1351 Issue *models.Issue 1352 Action string 1353} 1354 1355func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1356 params.Action = "edit" 1357 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1358} 1359 1360type ThreadReactionFragmentParams struct { 1361 Kind models.ReactionKind 1362 Count int 1363 Users []string 1364 IsReacted bool 1365 CommentRkey string 1366 SubjectUri string 1367} 1368 1369func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1370 return p.executePlain("repo/fragments/reaction", w, params) 1371} 1372 1373type RepoNewIssueParams struct { 1374 BaseParams 1375 RepoInfo repoinfo.RepoInfo 1376 Issue *models.Issue // existing issue if any -- passed when editing 1377 Active string 1378 Action string 1379} 1380 1381func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1382 params.Active = "issues" 1383 params.Action = "create" 1384 return p.executeRepo("repo/issues/new", w, params) 1385} 1386 1387type StackedDiff struct { 1388 Diff *types.NiceDiff 1389 Opts types.DiffOpts 1390} 1391 1392type RepoNewPullParams struct { 1393 BaseParams 1394 RepoInfo repoinfo.RepoInfo 1395 Branches []types.Branch 1396 SourceBranches []types.Branch 1397 ForkBranches []types.Branch 1398 Forks []models.Repo 1399 Source Source 1400 SourceBranch string 1401 TargetBranch string 1402 Fork string 1403 Patch string 1404 Title string 1405 Body string 1406 TitleDirty bool 1407 BodyDirty bool 1408 IsStacked bool 1409 Comparison *types.RepoFormatPatchResponse 1410 Diff *types.NiceDiff 1411 DiffOpts types.DiffOpts 1412 StackedDiffs []StackedDiff 1413 MergeCheck *types.MergeCheckResponse 1414 StackTitles map[string]string 1415 StackBodies map[string]string 1416 PrefillError string 1417 Active string 1418 LabelDefs map[string]*models.LabelDefinition 1419 LabelState models.LabelState 1420 StackLabelStates map[string]models.LabelState 1421} 1422 1423func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1424 params.Active = "pulls" 1425 return p.executeRepo("repo/pulls/new", w, params) 1426} 1427 1428func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1429 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1430} 1431 1432func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1433 return p.executePlain("fragments/markdownPreview", w, body) 1434} 1435 1436type RepoPullsParams struct { 1437 BaseParams 1438 RepoInfo repoinfo.RepoInfo 1439 Pulls []*models.Pull 1440 Active string 1441 FilterState string 1442 FilterQuery string 1443 BaseFilterQuery string 1444 Stacks []models.Stack 1445 Pipelines map[string]types.Pipeline 1446 LabelDefs map[string]*models.LabelDefinition 1447 Page pagination.Page 1448 PullCount int 1449 VouchRelationships map[syntax.DID]*models.VouchRelationship 1450} 1451 1452func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1453 params.Active = "pulls" 1454 return p.executeRepo("repo/pulls/pulls", w, params) 1455} 1456 1457type ResubmitResult uint64 1458 1459const ( 1460 ShouldResubmit ResubmitResult = iota 1461 ShouldNotResubmit 1462 Unknown 1463) 1464 1465func (r ResubmitResult) Yes() bool { 1466 return r == ShouldResubmit 1467} 1468func (r ResubmitResult) No() bool { 1469 return r == ShouldNotResubmit 1470} 1471func (r ResubmitResult) Unknown() bool { 1472 return r == Unknown 1473} 1474 1475type RepoSinglePullParams struct { 1476 BaseParams 1477 RepoInfo repoinfo.RepoInfo 1478 Active string 1479 Pull *models.Pull 1480 Stack models.Stack 1481 Backlinks []models.RichReferenceLink 1482 BranchDeleteStatus *models.BranchDeleteStatus 1483 MergeCheck types.MergeCheckResponse 1484 ResubmitCheck ResubmitResult 1485 Pipelines map[string]types.Pipeline 1486 Diff types.DiffRenderer 1487 DiffOpts types.DiffOpts 1488 ActiveRound int 1489 IsInterdiff bool 1490 1491 // WorkflowsChanged and ChangedWorkflowFiles describe whether the latest 1492 // round's patch touches .tangled/workflows/, for warning maintainers 1493 // before they manually trigger CI on a fork-based pull request. 1494 WorkflowsChanged bool 1495 ChangedWorkflowFiles []string 1496 1497 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1498 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1499 1500 LabelDefs map[string]*models.LabelDefinition 1501 VouchRelationships map[syntax.DID]*models.VouchRelationship 1502 VouchSkips map[syntax.DID]bool 1503} 1504 1505func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1506 params.Active = "pulls" 1507 return p.executeRepo("repo/pulls/pull", w, params) 1508} 1509 1510type PullResubmitParams struct { 1511 BaseParams 1512 RepoInfo repoinfo.RepoInfo 1513 Pull *models.Pull 1514 SubmissionId int 1515} 1516 1517func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1518 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1519} 1520 1521type PullActionsParams struct { 1522 BaseParams 1523 RepoInfo repoinfo.RepoInfo 1524 Pull *models.Pull 1525 RoundNumber int 1526 MergeCheck types.MergeCheckResponse 1527 ResubmitCheck ResubmitResult 1528 BranchDeleteStatus *models.BranchDeleteStatus 1529 Stack models.Stack 1530 1531 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1532 // that fetches the real, checked fragment 1533 Loading bool 1534} 1535 1536func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1537 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1538} 1539 1540type PullNewCommentParams struct { 1541 BaseParams 1542 RepoInfo repoinfo.RepoInfo 1543 Pull *models.Pull 1544 RoundNumber int 1545} 1546 1547func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1548 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1549} 1550 1551type RepoCompareParams struct { 1552 BaseParams 1553 RepoInfo repoinfo.RepoInfo 1554 Forks []models.Repo 1555 Branches []types.Branch 1556 Tags []*types.TagReference 1557 Base string 1558 Head string 1559 Diff *types.NiceDiff 1560 DiffOpts types.DiffOpts 1561 1562 Active string 1563} 1564 1565func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1566 params.Active = "overview" 1567 return p.executeRepo("repo/compare/compare", w, params) 1568} 1569 1570type RepoCompareNewParams struct { 1571 BaseParams 1572 RepoInfo repoinfo.RepoInfo 1573 Forks []models.Repo 1574 Branches []types.Branch 1575 Tags []*types.TagReference 1576 Base string 1577 Head string 1578 1579 Active string 1580} 1581 1582func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1583 params.Active = "overview" 1584 return p.executeRepo("repo/compare/new", w, params) 1585} 1586 1587type RepoCompareAllowPullParams struct { 1588 BaseParams 1589 RepoInfo repoinfo.RepoInfo 1590 Base string 1591 Head string 1592} 1593 1594func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1595 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1596} 1597 1598type RepoCompareDiffFragmentParams struct { 1599 Diff types.NiceDiff 1600 DiffOpts types.DiffOpts 1601} 1602 1603func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1604 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1605} 1606 1607type LabelPanelParams struct { 1608 BaseParams 1609 RepoInfo repoinfo.RepoInfo 1610 Defs map[string]*models.LabelDefinition 1611 Subject string 1612 State models.LabelState 1613} 1614 1615func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1616 return p.executePlain("repo/fragments/labelPanel", w, params) 1617} 1618 1619type EditLabelPanelParams struct { 1620 BaseParams 1621 RepoInfo repoinfo.RepoInfo 1622 Defs map[string]*models.LabelDefinition 1623 Subject string 1624 State models.LabelState 1625 Prefix string 1626} 1627 1628func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1629 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1630} 1631 1632type RepoStarsParams struct { 1633 BaseParams 1634 RepoInfo repoinfo.RepoInfo 1635 Active string 1636 Starrers []models.Star 1637 Page pagination.Page 1638 TotalCount int 1639} 1640 1641func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1642 params.Active = "overview" 1643 return p.executeRepo("repo/stars", w, params) 1644} 1645 1646type RepoForksParams struct { 1647 BaseParams 1648 RepoInfo repoinfo.RepoInfo 1649 Active string 1650 Forks []models.Repo 1651 Page pagination.Page 1652 TotalCount int 1653} 1654 1655func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1656 params.Active = "overview" 1657 return p.executeRepo("repo/forks", w, params) 1658} 1659 1660type PipelinesParams struct { 1661 BaseParams 1662 RepoInfo repoinfo.RepoInfo 1663 Pipelines []types.Pipeline 1664 Active string 1665 FilterKind string 1666 Total int64 1667} 1668 1669func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1670 params.Active = "pipelines" 1671 return p.executeRepo("repo/pipelines/pipelines", w, params) 1672} 1673 1674type LogBlockParams struct { 1675 Id int 1676 Name string 1677 Command string 1678 Collapsed bool 1679 StartTime time.Time 1680} 1681 1682func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1683 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1684} 1685 1686type LogBlockEndParams struct { 1687 Id int 1688 StartTime time.Time 1689 EndTime time.Time 1690} 1691 1692func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1693 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1694} 1695 1696type LogLineParams struct { 1697 Id int 1698 Content template.HTML 1699} 1700 1701func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1702 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1703} 1704 1705type WorkflowSymbolOOBParams struct { 1706 Name string 1707 Statuses models.WorkflowStatus 1708} 1709 1710func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1711 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1712} 1713 1714type WorkflowParams struct { 1715 BaseParams 1716 RepoInfo repoinfo.RepoInfo 1717 Pipeline types.Pipeline 1718 Workflow string 1719 LogUrl string 1720 Active string 1721} 1722 1723func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1724 params.Active = "pipelines" 1725 return p.executeRepo("repo/pipelines/workflow", w, params) 1726} 1727 1728type PutStringParams struct { 1729 BaseParams 1730 Action string 1731 1732 // this is supplied in the case of editing an existing string 1733 String models.String 1734} 1735 1736func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1737 return p.execute("strings/put", w, params) 1738} 1739 1740type StringsDashboardParams struct { 1741 BaseParams 1742 Card ProfileCard 1743 Strings []models.String 1744} 1745 1746func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1747 return p.execute("strings/dashboard", w, params) 1748} 1749 1750type StringTimelineParams struct { 1751 BaseParams 1752 Strings []models.String 1753} 1754 1755func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1756 return p.execute("strings/timeline", w, params) 1757} 1758 1759type SingleStringParams struct { 1760 BaseParams 1761 ShowRendered bool 1762 RenderToggle bool 1763 RenderedContents template.HTML 1764 String *models.String 1765 Stats models.StringStats 1766 IsStarred bool 1767 StarCount int 1768 Owner identity.Identity 1769 CommentList []models.CommentListItem 1770 1771 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1772 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1773 VouchRelationships map[syntax.DID]*models.VouchRelationship 1774} 1775 1776func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1777 return p.execute("strings/string", w, params) 1778} 1779 1780type SearchReposParams struct { 1781 BaseParams 1782 FilterType string // "repo" | "code" 1783 Repos []SearchResult 1784 Page pagination.Page 1785 ResultCount int 1786 FilterQuery string 1787 SortParam string 1788 TimeTaken time.Duration 1789 DocCount int64 1790 ErrorMsg string 1791} 1792 1793func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1794 params.FilterType = "repo" 1795 return p.execute("search/search", w, params) 1796} 1797 1798type SearchQuickParams struct { 1799 Repos []models.Repo 1800 Query string 1801 Total int 1802} 1803 1804func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1805 return p.executePlain("search/fragments/quick", w, params) 1806} 1807 1808func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1809 tpl, err := p.parse("search/fragments/quick") 1810 if err != nil { 1811 return err 1812 } 1813 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1814} 1815 1816type SearchResult struct { 1817 RepoDID syntax.DID 1818 Repo *models.Repo 1819 FilePath string 1820 Branches []string 1821 Commit string 1822 Language string 1823 1824 File *CodeSearchResult_File // filename match 1825 Chunks CodeSearchResult_Chunks // content matches 1826} 1827 1828// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1829type CodeSearchResult_Chunk struct { 1830 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1831 MatchCount int // number of match ranges in this chunk 1832} 1833 1834type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1835 1836func (cs CodeSearchResult_Chunks) MatchCount() int { 1837 count := 0 1838 for _, c := range cs { 1839 count += c.MatchCount 1840 } 1841 return count 1842} 1843 1844type CodeSearchResult_File struct { 1845 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1846} 1847 1848type ChunkSpan struct { 1849 Text string 1850 Match bool 1851} 1852 1853type ChunkLine struct { 1854 Num int 1855 Spans []ChunkSpan 1856 Highlight bool 1857} 1858 1859// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1860// line into matched/unmatched spans using ranges. startLine is the 1-based line 1861// number of the first line. 1862func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1863 if startLine < 1 { 1864 startLine = 1 1865 } 1866 // trim a single trailing newline so we don't emit a spurious empty line 1867 content = strings.TrimSuffix(content, "\n") 1868 lines := strings.Split(content, "\n") 1869 out := make([]ChunkLine, len(lines)) 1870 for i, text := range lines { 1871 num := startLine + i 1872 runes := []rune(text) 1873 1874 // collect matched rune intervals [c0,c1) for this line 1875 var intervals [][2]int 1876 for _, rg := range ranges { 1877 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1878 continue 1879 } 1880 c0, c1 := 0, len(runes) 1881 if num == int(rg.Start.LineNumber) { 1882 c0 = int(rg.Start.Column) - 1 1883 } 1884 if num == int(rg.End.LineNumber) { 1885 c1 = int(rg.End.Column) - 1 1886 } 1887 c0 = max(0, min(c0, len(runes))) 1888 c1 = max(0, min(c1, len(runes))) 1889 if c0 < c1 { 1890 intervals = append(intervals, [2]int{c0, c1}) 1891 } 1892 } 1893 intervals = mergeIntervals(intervals) 1894 1895 out[i] = ChunkLine{ 1896 Num: num, 1897 Spans: spanRunes(runes, intervals), 1898 Highlight: len(intervals) > 0, 1899 } 1900 } 1901 return out 1902} 1903 1904// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1905// Filename ranges live on line 1; columns are clamped to rune bounds. 1906func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1907 runes := []rune(name) 1908 var intervals [][2]int 1909 for _, rg := range ranges { 1910 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1911 continue 1912 } 1913 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1914 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1915 if c0 < c1 { 1916 intervals = append(intervals, [2]int{c0, c1}) 1917 } 1918 } 1919 return spanRunes(runes, mergeIntervals(intervals)) 1920} 1921 1922type CodeSearchParams struct { 1923 BaseParams 1924 FilterType string // "code" 1925 FilterQuery string 1926 Results []SearchResult 1927 Page pagination.Page 1928 HasMore bool 1929 ErrorMsg string 1930 1931 MatchCount int 1932 FileCount int 1933 TimeTaken time.Duration 1934} 1935 1936func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1937 params.FilterType = "code" 1938 return p.execute("search/search", w, params) 1939} 1940 1941func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1942 return p.execute("timeline/home", w, params) 1943} 1944 1945type CommentBodyFragmentParams struct { 1946 Comment models.Comment 1947 Reactions map[models.ReactionKind]models.ReactionDisplayData 1948 UserReacted map[models.ReactionKind]bool 1949} 1950 1951func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1952 return p.executePlain("fragments/comment/commentBody", w, params) 1953} 1954 1955type PullCommentFragmentParams struct { 1956 LoggedInUser *oauth.MultiAccountUser 1957 Comment models.Comment 1958 Reactions map[models.ReactionKind]models.ReactionDisplayData 1959 UserReacted map[models.ReactionKind]bool 1960 HxSwapOob bool 1961} 1962 1963func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error { 1964 return p.executePlain("fragments/comment/pullComment", w, params) 1965} 1966 1967type CommentHeaderFragmentParams struct { 1968 Comment models.Comment 1969 Reactions map[models.ReactionKind]models.ReactionDisplayData 1970 UserReacted map[models.ReactionKind]bool 1971 HxSwapOob bool 1972} 1973 1974func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 1975 return p.executePlain("fragments/comment/commentHeader", w, params) 1976} 1977 1978type EditCommentFragmentParams struct { 1979 Comment models.Comment 1980} 1981 1982func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1983 return p.executePlain("fragments/comment/edit", w, params) 1984} 1985 1986type ReplyCommentFragmentParams struct { 1987 BaseParams 1988} 1989 1990func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1991 return p.executePlain("fragments/comment/reply", w, params) 1992} 1993 1994type ReplyPlaceholderFragmentParams struct { 1995 BaseParams 1996} 1997 1998func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1999 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 2000} 2001 2002func (p *Pages) Static() http.Handler { 2003 if p.dev { 2004 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 2005 } 2006 2007 sub, err := fs.Sub(p.embedFS, "static") 2008 if err != nil { 2009 p.logger.Error("no static dir found? that's crazy", "err", err) 2010 panic(err) 2011 } 2012 // Custom handler to apply Cache-Control headers for font files 2013 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 2014} 2015 2016func Cache(h http.Handler) http.Handler { 2017 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2018 path := strings.Split(r.URL.Path, "?")[0] 2019 2020 if strings.HasSuffix(path, ".css") { 2021 // on day for css files 2022 w.Header().Set("Cache-Control", "public, max-age=86400") 2023 } else { 2024 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 2025 } 2026 h.ServeHTTP(w, r) 2027 }) 2028} 2029 2030func (p *Pages) CssContentHash() string { 2031 cssFile, err := p.embedFS.Open("static/tw.css") 2032 if err != nil { 2033 slog.Debug("Error opening CSS file", "err", err) 2034 return "" 2035 } 2036 defer cssFile.Close() 2037 2038 hasher := sha256.New() 2039 if _, err := io.Copy(hasher, cssFile); err != nil { 2040 slog.Debug("Error hashing CSS file", "err", err) 2041 return "" 2042 } 2043 2044 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 2045} 2046 2047func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 2048 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 2049} 2050 2051func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 2052 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 2053} 2054 2055func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 2056 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 2057} 2058 2059func (p *Pages) Error500(w io.Writer) error { 2060 return p.execute("errors/500", w, nil) 2061} 2062 2063func (p *Pages) Error404(w io.Writer) error { 2064 return p.execute("errors/404", w, nil) 2065} 2066 2067func (p *Pages) ErrorKnot404(w io.Writer) error { 2068 return p.execute("errors/knot404", w, nil) 2069} 2070 2071func (p *Pages) Error503(w io.Writer) error { 2072 return p.execute("errors/503", w, nil) 2073}