Monorepo for Tangled
0

Configure Feed

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

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