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
54 kB 2119 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 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} 709 710func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 711 return p.execute("repo/new", w, params) 712} 713 714type ForkRepoParams struct { 715 BaseParams 716 Knots []string 717 RepoInfo repoinfo.RepoInfo 718} 719 720func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 721 return p.execute("repo/fork", w, params) 722} 723 724type ProfileCard struct { 725 UserDid string 726 HasProfile bool 727 IsTangledUser bool 728 FollowStatus models.FollowStatus 729 VouchRelationship *models.VouchRelationship 730 Punchcard *models.Punchcard 731 Profile *models.Profile 732 Stats ProfileStats 733 Active string 734 ProfileScript string 735} 736 737type ProfileStats struct { 738 RepoCount int64 739 StarredCount int64 740 StringCount int64 741 FollowersCount int64 742 FollowingCount int64 743} 744 745func (p *ProfileCard) GetTabs() [][]any { 746 tabs := [][]any{ 747 {"overview", "overview", "square-chart-gantt", nil}, 748 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 749 {"starred", "starred", "star", p.Stats.StarredCount}, 750 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 751 {"vouches", "vouches", "shield", nil}, 752 } 753 754 return tabs 755} 756 757type ProfileOverviewParams struct { 758 BaseParams 759 Repos []models.Repo 760 CollaboratingRepos []models.Repo 761 ProfileTimeline *models.ProfileTimeline 762 Card *ProfileCard 763 Active string 764 ShowPunchcard bool 765} 766 767func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 768 params.Active = "overview" 769 return p.executeProfile("user/overview", w, params) 770} 771 772type ProfileReposParams struct { 773 BaseParams 774 Repos []models.Repo 775 StarStatuses map[string]bool 776 Card *ProfileCard 777 Active string 778 Page pagination.Page 779 RepoCount int 780 FilterQuery string 781} 782 783func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 784 params.Active = "repos" 785 return p.executeProfile("user/repos", w, params) 786} 787 788type ProfileStarredParams struct { 789 BaseParams 790 Repos []models.Repo 791 Card *ProfileCard 792 Page pagination.Page 793 Total int 794 Active string 795} 796 797func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 798 params.Active = "starred" 799 return p.executeProfile("user/starred", w, params) 800} 801 802type ProfileStringsParams struct { 803 BaseParams 804 Strings []models.String 805 Card *ProfileCard 806 Active string 807} 808 809func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 810 params.Active = "strings" 811 return p.executeProfile("user/strings", w, params) 812} 813 814type ProfileVouchesParams struct { 815 BaseParams 816 Vouches []models.Vouch 817 Suggestions []models.VouchSuggestion 818 Card *ProfileCard 819 Page pagination.Page 820 VouchCount int 821 Active string 822 EvidencePulls map[syntax.ATURI]*models.Pull 823 EvidenceIssues map[syntax.ATURI]*models.Issue 824} 825 826func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 827 params.Active = "vouches" 828 return p.executeProfile("user/vouches", w, params) 829} 830 831type FollowCard struct { 832 UserDid string 833 BaseParams 834 FollowStatus models.FollowStatus 835 FollowersCount int64 836 FollowingCount int64 837 Profile *models.Profile 838} 839 840type ProfileFollowersParams struct { 841 BaseParams 842 Followers []FollowCard 843 Card *ProfileCard 844 Active string 845} 846 847func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 848 params.Active = "overview" 849 return p.executeProfile("user/followers", w, params) 850} 851 852type ProfileFollowingParams struct { 853 BaseParams 854 Following []FollowCard 855 Card *ProfileCard 856 Active string 857} 858 859func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 860 params.Active = "overview" 861 return p.executeProfile("user/following", w, params) 862} 863 864type FollowFragmentParams struct { 865 UserDid string 866 FollowStatus models.FollowStatus 867 FollowersCount int64 868 HxSwapOob struct{} // empty struct so always truthy 869} 870 871func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 872 return p.executePlain("user/fragments/follow-oob", w, params) 873} 874 875type ProfilePopoverParams struct { 876 BaseParams 877 UserDid string 878 Profile *models.Profile 879 FollowStatus models.FollowStatus 880 VouchRelationship *models.VouchRelationship 881 Stats ProfilePopoverStats 882} 883 884type ProfilePopoverStats struct { 885 FollowersCount int64 886 FollowingCount int64 887} 888 889func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 890 return p.executePlain("user/fragments/profilePopover", w, params) 891} 892 893type EditBioParams struct { 894 BaseParams 895 Profile *models.Profile 896 AlsoKnownAs []string 897 // Action optionally overrides the form's hx-post target. Defaults to 898 // /profile/bio when empty. Used by the onboarding flow to save + advance. 899 Action string 900} 901 902func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 903 return p.executePlain("user/fragments/editBio", w, params) 904} 905 906type EditPinsParams struct { 907 BaseParams 908 Profile *models.Profile 909 AllRepos []PinnedRepo 910} 911 912type PinnedRepo struct { 913 IsPinned bool 914 models.Repo 915} 916 917func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 918 return p.executePlain("user/fragments/editPins", w, params) 919} 920 921type StarBtnFragmentParams struct { 922 IsStarred bool 923 SubjectAt syntax.ATURI 924 StarCount int 925 RepoName string 926 HxSwapOob bool 927} 928 929func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 930 params.HxSwapOob = true 931 return p.executePlain("fragments/starBtn", w, params) 932} 933 934type OnboardingParams struct { 935 BaseParams 936 Step int 937 938 EditBio EditBioParams 939 940 PubKeys []models.PublicKey 941 942 People []FollowCard 943 TrendingRepos []models.Repo 944 StarStatuses map[string]bool 945} 946 947// KeyFragment renders a single SSH key row (used to append a newly added key 948// to the list without a full reload). 949func (p *Pages) KeyFragment(w io.Writer, key models.PublicKey) error { 950 return p.executePlain("user/settings/fragments/keyListing", w, key) 951} 952 953func (p *Pages) Onboarding(w io.Writer, params OnboardingParams) error { 954 return p.executeOnboarding("onboarding/welcome", w, params) 955} 956 957type RepoIndexParams struct { 958 BaseParams 959 RepoInfo repoinfo.RepoInfo 960 Active string 961 TagMap map[string][]string 962 CommitsTrunc []types.Commit 963 TagsTrunc []*types.TagReference 964 BranchesTrunc []types.Branch 965 // ForkInfo *types.ForkInfo 966 HTMLReadme template.HTML 967 Raw bool 968 EmailToDid map[string]string 969 VerifiedCommits commitverify.VerifiedCommits 970 Languages []types.RepoLanguageDetails 971 NeedsKnotUpgrade bool 972 KnotUnreachable bool 973 types.RepoIndexResponse 974} 975 976func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 977 params.Active = "overview" 978 if params.IsEmpty { 979 return p.executeRepo("repo/empty", w, params) 980 } 981 982 if params.NeedsKnotUpgrade { 983 return p.executeRepo("repo/needsUpgrade", w, params) 984 } 985 986 if params.KnotUnreachable { 987 return p.executeRepo("repo/knotUnreachable", w, params) 988 } 989 990 rctx := p.rctx.Clone() 991 rctx.RepoInfo = params.RepoInfo 992 rctx.RepoInfo.Ref = params.Ref 993 rctx.RendererType = markup.RendererTypeRepoMarkdown 994 995 if params.ReadmeFileName != "" { 996 switch markup.GetFormat(params.ReadmeFileName) { 997 case markup.FormatMarkdown: 998 params.Raw = false 999 htmlString := rctx.RenderMarkdown(params.Readme) 1000 sanitized := sanitizer.SanitizeDefault(htmlString) 1001 params.HTMLReadme = template.HTML(sanitized) 1002 default: 1003 params.Raw = true 1004 } 1005 } 1006 1007 return p.executeRepo("repo/index", w, params) 1008} 1009 1010type RepoSearchParams struct { 1011 BaseParams 1012 RepoInfo repoinfo.RepoInfo 1013 Active string 1014 FilterQuery string 1015} 1016 1017func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 1018 params.Active = "overview" 1019 return p.executeRepo("repo/search", w, params) 1020} 1021 1022type RepoSearchResultsFragmentParams struct { 1023 Query string 1024 Results []SearchResult 1025 ErrorMsg string 1026} 1027 1028func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 1029 return p.executePlain("repo/fragments/searchResults", w, params) 1030} 1031 1032type RepoLogParams struct { 1033 BaseParams 1034 RepoInfo repoinfo.RepoInfo 1035 TagMap map[string][]string 1036 Active string 1037 EmailToDid map[string]string 1038 VerifiedCommits commitverify.VerifiedCommits 1039 1040 types.RepoLogResponse 1041} 1042 1043func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 1044 params.Active = "overview" 1045 return p.executeRepo("repo/log", w, params) 1046} 1047 1048type RepoCommitParams struct { 1049 BaseParams 1050 RepoInfo repoinfo.RepoInfo 1051 Active string 1052 EmailToDid map[string]string 1053 Pipeline *types.Pipeline 1054 DiffOpts types.DiffOpts 1055 1056 // singular because it's always going to be just one 1057 VerifiedCommit commitverify.VerifiedCommits 1058 1059 types.RepoCommitResponse 1060} 1061 1062func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 1063 params.Active = "overview" 1064 return p.executeRepo("repo/commit", w, params) 1065} 1066 1067type RepoTreeParams struct { 1068 BaseParams 1069 RepoInfo repoinfo.RepoInfo 1070 Active string 1071 BreadCrumbs [][]string 1072 Path string 1073 Raw bool 1074 HTMLReadme template.HTML 1075 EmailToDid map[string]string 1076 LastCommitInfo *types.LastCommitInfo 1077 Ref string 1078 Parent string 1079 DotDot string 1080 Files []types.NiceTree 1081 ReadmeFileName string 1082 Readme string 1083} 1084 1085type RepoTreeStats struct { 1086 NumFolders uint64 1087 NumFiles uint64 1088} 1089 1090func (r RepoTreeParams) TreeStats() RepoTreeStats { 1091 numFolders, numFiles := 0, 0 1092 for _, f := range r.Files { 1093 if !f.IsFile() { 1094 numFolders += 1 1095 } else if f.IsFile() { 1096 numFiles += 1 1097 } 1098 } 1099 1100 return RepoTreeStats{ 1101 NumFolders: uint64(numFolders), 1102 NumFiles: uint64(numFiles), 1103 } 1104} 1105 1106func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 1107 params.Active = "overview" 1108 1109 rctx := p.rctx.Clone() 1110 rctx.RepoInfo = params.RepoInfo 1111 rctx.RepoInfo.Ref = params.Ref 1112 rctx.RendererType = markup.RendererTypeRepoMarkdown 1113 1114 if params.ReadmeFileName != "" { 1115 switch markup.GetFormat(params.ReadmeFileName) { 1116 case markup.FormatMarkdown: 1117 params.Raw = false 1118 htmlString := rctx.RenderMarkdown(params.Readme) 1119 sanitized := sanitizer.SanitizeDefault(htmlString) 1120 params.HTMLReadme = template.HTML(sanitized) 1121 default: 1122 params.Raw = true 1123 } 1124 } 1125 1126 return p.executeRepo("repo/tree", w, params) 1127} 1128 1129type RepoBranchesParams struct { 1130 BaseParams 1131 RepoInfo repoinfo.RepoInfo 1132 Active string 1133 types.RepoBranchesResponse 1134} 1135 1136func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 1137 params.Active = "overview" 1138 return p.executeRepo("repo/branches", w, params) 1139} 1140 1141type RepoTagsParams struct { 1142 BaseParams 1143 RepoInfo repoinfo.RepoInfo 1144 Active string 1145 types.RepoTagsResponse 1146 ArtifactMap map[plumbing.Hash][]models.Artifact 1147 DanglingArtifacts []models.Artifact 1148} 1149 1150func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1151 params.Active = "overview" 1152 return p.executeRepo("repo/tags", w, params) 1153} 1154 1155type RepoTagParams struct { 1156 BaseParams 1157 RepoInfo repoinfo.RepoInfo 1158 Active string 1159 types.RepoTagResponse 1160 ArtifactMap map[plumbing.Hash][]models.Artifact 1161 DanglingArtifacts []models.Artifact 1162} 1163 1164func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1165 params.Active = "overview" 1166 return p.executeRepo("repo/tag", w, params) 1167} 1168 1169type RepoArtifactParams struct { 1170 BaseParams 1171 RepoInfo repoinfo.RepoInfo 1172 Artifact models.Artifact 1173} 1174 1175func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1176 return p.executePlain("repo/fragments/artifact", w, params) 1177} 1178 1179type RepoBlobParams struct { 1180 BaseParams 1181 RepoInfo repoinfo.RepoInfo 1182 Active string // always "overview" 1183 BreadCrumbs [][]string 1184 BlobView models.BlobView // TODO: expose this struct 1185 ShowRendered bool 1186 EmailToDid map[string]string 1187 LastCommitInfo *types.LastCommitInfo 1188 Ref string 1189 Path string 1190 Language string 1191} 1192 1193func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1194 params.Active = "overview" 1195 return p.executeRepo("repo/blob", w, params) 1196} 1197 1198type Collaborator struct { 1199 Did string 1200 Role string 1201} 1202 1203type RepoSettingsParams struct { 1204 BaseParams 1205 RepoInfo repoinfo.RepoInfo 1206 Collaborators []Collaborator 1207 Active string 1208 Branches []types.Branch 1209 Spindles []string 1210 CurrentSpindle string 1211 Secrets []*tangled.RepoListSecrets_Secret 1212 1213 // TODO: use repoinfo.roles 1214 IsCollaboratorInviteAllowed bool 1215} 1216 1217func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1218 params.Active = "settings" 1219 return p.executeRepo("repo/settings", w, params) 1220} 1221 1222type RepoGeneralSettingsParams struct { 1223 BaseParams 1224 RepoInfo repoinfo.RepoInfo 1225 Labels []models.LabelDefinition 1226 DefaultLabels []models.LabelDefinition 1227 SubscribedLabels map[string]struct{} 1228 ShouldSubscribeAll bool 1229 Active string 1230 Tab string 1231 Branches []types.Branch 1232} 1233 1234func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1235 params.Active = "settings" 1236 params.Tab = "general" 1237 return p.executeRepo("repo/settings/general", w, params) 1238} 1239 1240type RepoAccessSettingsParams struct { 1241 BaseParams 1242 RepoInfo repoinfo.RepoInfo 1243 Active string 1244 Tab string 1245 Collaborators []Collaborator 1246 CanRemoveCollaborator bool 1247} 1248 1249func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1250 params.Active = "settings" 1251 params.Tab = "access" 1252 return p.executeRepo("repo/settings/access", w, params) 1253} 1254 1255type RepoPipelineSettingsParams struct { 1256 BaseParams 1257 RepoInfo repoinfo.RepoInfo 1258 Active string 1259 Tab string 1260 Spindles []string 1261 CurrentSpindle string 1262 Secrets []map[string]any 1263} 1264 1265func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1266 params.Active = "settings" 1267 params.Tab = "pipelines" 1268 return p.executeRepo("repo/settings/pipelines", w, params) 1269} 1270 1271type RepoWebhooksSettingsParams struct { 1272 BaseParams 1273 RepoInfo repoinfo.RepoInfo 1274 Active string 1275 Tab string 1276 Webhooks []models.Webhook 1277 WebhookDeliveries map[int64][]models.WebhookDelivery 1278} 1279 1280func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1281 params.Active = "settings" 1282 params.Tab = "hooks" 1283 return p.executeRepo("repo/settings/hooks", w, params) 1284} 1285 1286type WebhookDeliveriesListParams struct { 1287 BaseParams 1288 RepoInfo repoinfo.RepoInfo 1289 Webhook *models.Webhook 1290 Deliveries []models.WebhookDelivery 1291} 1292 1293func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1294 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1295 if err != nil { 1296 return err 1297 } 1298 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1299} 1300 1301type RepoSiteSettingsParams struct { 1302 BaseParams 1303 RepoInfo repoinfo.RepoInfo 1304 Active string 1305 Tab string 1306 Branches []types.Branch 1307 SiteConfig *models.RepoSite 1308 OwnerClaim *models.DomainClaim 1309 Deploys []models.SiteDeploy 1310 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1311} 1312 1313func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1314 params.Active = "settings" 1315 params.Tab = "sites" 1316 return p.executeRepo("repo/settings/sites", w, params) 1317} 1318 1319type RepoIssuesParams struct { 1320 BaseParams 1321 RepoInfo repoinfo.RepoInfo 1322 Active string 1323 Issues []models.Issue 1324 IssueCount int 1325 LabelDefs map[string]*models.LabelDefinition 1326 Page pagination.Page 1327 FilterState string 1328 FilterQuery string 1329 BaseFilterQuery string 1330 VouchRelationships map[syntax.DID]*models.VouchRelationship 1331} 1332 1333func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1334 params.Active = "issues" 1335 return p.executeRepo("repo/issues/issues", w, params) 1336} 1337 1338type RepoSingleIssueParams struct { 1339 BaseParams 1340 RepoInfo repoinfo.RepoInfo 1341 Active string 1342 Issue *models.Issue 1343 CommentList []models.CommentListItem 1344 Backlinks []models.RichReferenceLink 1345 LabelDefs map[string]*models.LabelDefinition 1346 1347 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1348 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1349 VouchRelationships map[syntax.DID]*models.VouchRelationship 1350 1351 // IsSubscribed is nil when the user is not logged in, true when subscribed, 1352 // false when explicitly unsubscribed, and nil when no explicit subscription. 1353 IsSubscribed *bool 1354} 1355 1356func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1357 params.Active = "issues" 1358 return p.executeRepo("repo/issues/issue", w, params) 1359} 1360 1361type IssueSubscribeParams struct { 1362 RepoInfo repoinfo.RepoInfo 1363 IssueId int 1364 IsSubscribed *bool 1365} 1366 1367func (p *Pages) IssueSubscribeFragment(w io.Writer, params IssueSubscribeParams) error { 1368 return p.executePlain("repo/issues/fragments/subscribeButton", w, params) 1369} 1370 1371type PullSubscribeParams struct { 1372 RepoInfo repoinfo.RepoInfo 1373 PullId int 1374 IsSubscribed *bool 1375} 1376 1377func (p *Pages) PullSubscribeFragment(w io.Writer, params PullSubscribeParams) error { 1378 return p.executePlain("repo/pulls/fragments/subscribeButton", w, params) 1379} 1380 1381type EditIssueParams struct { 1382 BaseParams 1383 RepoInfo repoinfo.RepoInfo 1384 Issue *models.Issue 1385 Action string 1386} 1387 1388func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1389 params.Action = "edit" 1390 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1391} 1392 1393type ThreadReactionFragmentParams struct { 1394 Kind models.ReactionKind 1395 Count int 1396 Users []string 1397 IsReacted bool 1398 CommentRkey string 1399 SubjectUri string 1400} 1401 1402func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1403 return p.executePlain("repo/fragments/reaction", w, params) 1404} 1405 1406type RepoNewIssueParams struct { 1407 BaseParams 1408 RepoInfo repoinfo.RepoInfo 1409 Issue *models.Issue // existing issue if any -- passed when editing 1410 Active string 1411 Action string 1412} 1413 1414func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1415 params.Active = "issues" 1416 params.Action = "create" 1417 return p.executeRepo("repo/issues/new", w, params) 1418} 1419 1420type StackedDiff struct { 1421 Diff *types.NiceDiff 1422 Opts types.DiffOpts 1423} 1424 1425type RepoNewPullParams struct { 1426 BaseParams 1427 RepoInfo repoinfo.RepoInfo 1428 Branches []types.Branch 1429 SourceBranches []types.Branch 1430 ForkBranches []types.Branch 1431 Forks []models.Repo 1432 Source Source 1433 SourceBranch string 1434 TargetBranch string 1435 Fork string 1436 Patch string 1437 Title string 1438 Body string 1439 TitleDirty bool 1440 BodyDirty bool 1441 IsStacked bool 1442 Comparison *types.RepoFormatPatchResponse 1443 Diff *types.NiceDiff 1444 DiffOpts types.DiffOpts 1445 StackedDiffs []StackedDiff 1446 MergeCheck *types.MergeCheckResponse 1447 StackTitles map[string]string 1448 StackBodies map[string]string 1449 PrefillError string 1450 Active string 1451 LabelDefs map[string]*models.LabelDefinition 1452 LabelState models.LabelState 1453 StackLabelStates map[string]models.LabelState 1454} 1455 1456func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1457 params.Active = "pulls" 1458 return p.executeRepo("repo/pulls/new", w, params) 1459} 1460 1461func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1462 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1463} 1464 1465func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1466 return p.executePlain("fragments/markdownPreview", w, body) 1467} 1468 1469type RepoPullsParams struct { 1470 BaseParams 1471 RepoInfo repoinfo.RepoInfo 1472 Pulls []*models.Pull 1473 Active string 1474 FilterState string 1475 FilterQuery string 1476 BaseFilterQuery string 1477 Stacks []models.Stack 1478 Pipelines map[string]types.Pipeline 1479 LabelDefs map[string]*models.LabelDefinition 1480 Page pagination.Page 1481 PullCount int 1482 VouchRelationships map[syntax.DID]*models.VouchRelationship 1483} 1484 1485func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1486 params.Active = "pulls" 1487 return p.executeRepo("repo/pulls/pulls", w, params) 1488} 1489 1490type ResubmitResult uint64 1491 1492const ( 1493 ShouldResubmit ResubmitResult = iota 1494 ShouldNotResubmit 1495 Unknown 1496) 1497 1498func (r ResubmitResult) Yes() bool { 1499 return r == ShouldResubmit 1500} 1501func (r ResubmitResult) No() bool { 1502 return r == ShouldNotResubmit 1503} 1504func (r ResubmitResult) Unknown() bool { 1505 return r == Unknown 1506} 1507 1508type RepoSinglePullParams struct { 1509 BaseParams 1510 RepoInfo repoinfo.RepoInfo 1511 Active string 1512 Pull *models.Pull 1513 Stack models.Stack 1514 Backlinks []models.RichReferenceLink 1515 BranchDeleteStatus *models.BranchDeleteStatus 1516 MergeCheck types.MergeCheckResponse 1517 ResubmitCheck ResubmitResult 1518 Pipelines map[string]types.Pipeline 1519 Diff types.DiffRenderer 1520 DiffOpts types.DiffOpts 1521 ActiveRound int 1522 IsInterdiff bool 1523 1524 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1525 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1526 1527 LabelDefs map[string]*models.LabelDefinition 1528 VouchRelationships map[syntax.DID]*models.VouchRelationship 1529 VouchSkips map[syntax.DID]bool 1530 1531 // IsSubscribed is nil when not logged in, true when subscribed, false when explicitly unsubscribed. 1532 IsSubscribed *bool 1533} 1534 1535func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1536 params.Active = "pulls" 1537 return p.executeRepo("repo/pulls/pull", w, params) 1538} 1539 1540type PullResubmitParams struct { 1541 BaseParams 1542 RepoInfo repoinfo.RepoInfo 1543 Pull *models.Pull 1544 SubmissionId int 1545} 1546 1547func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1548 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1549} 1550 1551type PullActionsParams struct { 1552 BaseParams 1553 RepoInfo repoinfo.RepoInfo 1554 Pull *models.Pull 1555 RoundNumber int 1556 MergeCheck types.MergeCheckResponse 1557 ResubmitCheck ResubmitResult 1558 BranchDeleteStatus *models.BranchDeleteStatus 1559 Stack models.Stack 1560 1561 // Workflow warning state for fork-based pulls without a pipeline on the 1562 // latest commit. WorkflowsChanged and ChangedWorkflowFiles are computed 1563 // from the latest round's patch. 1564 WorkflowsChanged bool 1565 ChangedWorkflowFiles []string 1566 HasPipeline bool 1567 1568 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1569 // that fetches the real, checked fragment 1570 Loading bool 1571} 1572 1573func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1574 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1575} 1576 1577type PullNewCommentParams struct { 1578 BaseParams 1579 RepoInfo repoinfo.RepoInfo 1580 Pull *models.Pull 1581 RoundNumber int 1582} 1583 1584func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1585 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1586} 1587 1588type RepoCompareParams struct { 1589 BaseParams 1590 RepoInfo repoinfo.RepoInfo 1591 Forks []models.Repo 1592 Branches []types.Branch 1593 Tags []*types.TagReference 1594 Base string 1595 Head string 1596 Diff *types.NiceDiff 1597 DiffOpts types.DiffOpts 1598 1599 Active string 1600} 1601 1602func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1603 params.Active = "overview" 1604 return p.executeRepo("repo/compare/compare", w, params) 1605} 1606 1607type RepoCompareNewParams struct { 1608 BaseParams 1609 RepoInfo repoinfo.RepoInfo 1610 Forks []models.Repo 1611 Branches []types.Branch 1612 Tags []*types.TagReference 1613 Base string 1614 Head string 1615 1616 Active string 1617} 1618 1619func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1620 params.Active = "overview" 1621 return p.executeRepo("repo/compare/new", w, params) 1622} 1623 1624type RepoCompareAllowPullParams struct { 1625 BaseParams 1626 RepoInfo repoinfo.RepoInfo 1627 Base string 1628 Head string 1629} 1630 1631func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1632 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1633} 1634 1635type RepoCompareDiffFragmentParams struct { 1636 Diff types.NiceDiff 1637 DiffOpts types.DiffOpts 1638} 1639 1640func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1641 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1642} 1643 1644type LabelPanelParams struct { 1645 BaseParams 1646 RepoInfo repoinfo.RepoInfo 1647 Defs map[string]*models.LabelDefinition 1648 Subject string 1649 State models.LabelState 1650} 1651 1652func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1653 return p.executePlain("repo/fragments/labelPanel", w, params) 1654} 1655 1656type EditLabelPanelParams struct { 1657 BaseParams 1658 RepoInfo repoinfo.RepoInfo 1659 Defs map[string]*models.LabelDefinition 1660 Subject string 1661 State models.LabelState 1662 Prefix string 1663} 1664 1665func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1666 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1667} 1668 1669type RepoStarsParams struct { 1670 BaseParams 1671 RepoInfo repoinfo.RepoInfo 1672 Active string 1673 Starrers []models.Star 1674 Page pagination.Page 1675 TotalCount int 1676} 1677 1678func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1679 params.Active = "overview" 1680 return p.executeRepo("repo/stars", w, params) 1681} 1682 1683type RepoForksParams struct { 1684 BaseParams 1685 RepoInfo repoinfo.RepoInfo 1686 Active string 1687 Forks []models.Repo 1688 Page pagination.Page 1689 TotalCount int 1690} 1691 1692func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1693 params.Active = "overview" 1694 return p.executeRepo("repo/forks", w, params) 1695} 1696 1697type PipelinesParams struct { 1698 BaseParams 1699 RepoInfo repoinfo.RepoInfo 1700 Pipelines []types.Pipeline 1701 Active string 1702 FilterKind string 1703 Total int64 1704} 1705 1706func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1707 params.Active = "pipelines" 1708 return p.executeRepo("repo/pipelines/pipelines", w, params) 1709} 1710 1711type LogBlockParams struct { 1712 Id int 1713 Name string 1714 Command string 1715 Collapsed bool 1716 StartTime time.Time 1717} 1718 1719func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1720 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1721} 1722 1723type LogBlockEndParams struct { 1724 Id int 1725 StartTime time.Time 1726 EndTime time.Time 1727} 1728 1729func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1730 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1731} 1732 1733type LogLineParams struct { 1734 Id int 1735 Content template.HTML 1736} 1737 1738func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1739 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1740} 1741 1742type WorkflowSymbolOOBParams struct { 1743 Name string 1744 Statuses models.WorkflowStatus 1745} 1746 1747func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1748 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1749} 1750 1751type PipelineStatusesParams struct { 1752 RepoInfo repoinfo.RepoInfo 1753 Pipelines map[string]types.Pipeline 1754} 1755 1756func (p *Pages) PipelineStatusesFragment(w io.Writer, params PipelineStatusesParams) error { 1757 return p.executePlain("repo/fragments/commitPipelineStatuses", w, params) 1758} 1759 1760type WorkflowParams struct { 1761 BaseParams 1762 RepoInfo repoinfo.RepoInfo 1763 Pipeline types.Pipeline 1764 Workflow string 1765 SSHLogCommand string 1766 Active string 1767} 1768 1769func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1770 params.Active = "pipelines" 1771 return p.executeRepo("repo/pipelines/workflow", w, params) 1772} 1773 1774type PutStringParams struct { 1775 BaseParams 1776 Action string 1777 1778 // this is supplied in the case of editing an existing string 1779 String models.String 1780} 1781 1782func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1783 return p.execute("strings/put", w, params) 1784} 1785 1786type StringsDashboardParams struct { 1787 BaseParams 1788 Card ProfileCard 1789 Strings []models.String 1790} 1791 1792func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1793 return p.execute("strings/dashboard", w, params) 1794} 1795 1796type StringTimelineParams struct { 1797 BaseParams 1798 Strings []models.String 1799} 1800 1801func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1802 return p.execute("strings/timeline", w, params) 1803} 1804 1805type SingleStringParams struct { 1806 BaseParams 1807 ShowRendered bool 1808 RenderToggle bool 1809 RenderedContents template.HTML 1810 String *models.String 1811 Stats models.StringStats 1812 IsStarred bool 1813 StarCount int 1814 Owner identity.Identity 1815 CommentList []models.CommentListItem 1816 1817 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1818 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1819 VouchRelationships map[syntax.DID]*models.VouchRelationship 1820} 1821 1822func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1823 return p.execute("strings/string", w, params) 1824} 1825 1826type SearchReposParams struct { 1827 BaseParams 1828 FilterType string // "repo" | "code" 1829 Repos []SearchResult 1830 Page pagination.Page 1831 ResultCount int 1832 FilterQuery string 1833 SortParam string 1834 TimeTaken time.Duration 1835 DocCount int64 1836 ErrorMsg string 1837} 1838 1839func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1840 params.FilterType = "repo" 1841 return p.execute("search/search", w, params) 1842} 1843 1844type SearchQuickParams struct { 1845 Repos []models.Repo 1846 Query string 1847 Total int 1848} 1849 1850func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1851 return p.executePlain("search/fragments/quick", w, params) 1852} 1853 1854func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1855 tpl, err := p.parse("search/fragments/quick") 1856 if err != nil { 1857 return err 1858 } 1859 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1860} 1861 1862type SearchResult struct { 1863 RepoDID syntax.DID 1864 Repo *models.Repo 1865 FilePath string 1866 Branches []string 1867 Commit string 1868 Language string 1869 1870 File *CodeSearchResult_File // filename match 1871 Chunks CodeSearchResult_Chunks // content matches 1872} 1873 1874// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1875type CodeSearchResult_Chunk struct { 1876 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1877 MatchCount int // number of match ranges in this chunk 1878} 1879 1880type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1881 1882func (cs CodeSearchResult_Chunks) MatchCount() int { 1883 count := 0 1884 for _, c := range cs { 1885 count += c.MatchCount 1886 } 1887 return count 1888} 1889 1890type CodeSearchResult_File struct { 1891 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1892} 1893 1894type ChunkSpan struct { 1895 Text string 1896 Match bool 1897} 1898 1899type ChunkLine struct { 1900 Num int 1901 Spans []ChunkSpan 1902 Highlight bool 1903} 1904 1905// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1906// line into matched/unmatched spans using ranges. startLine is the 1-based line 1907// number of the first line. 1908func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1909 if startLine < 1 { 1910 startLine = 1 1911 } 1912 // trim a single trailing newline so we don't emit a spurious empty line 1913 content = strings.TrimSuffix(content, "\n") 1914 lines := strings.Split(content, "\n") 1915 out := make([]ChunkLine, len(lines)) 1916 for i, text := range lines { 1917 num := startLine + i 1918 runes := []rune(text) 1919 1920 // collect matched rune intervals [c0,c1) for this line 1921 var intervals [][2]int 1922 for _, rg := range ranges { 1923 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1924 continue 1925 } 1926 c0, c1 := 0, len(runes) 1927 if num == int(rg.Start.LineNumber) { 1928 c0 = int(rg.Start.Column) - 1 1929 } 1930 if num == int(rg.End.LineNumber) { 1931 c1 = int(rg.End.Column) - 1 1932 } 1933 c0 = max(0, min(c0, len(runes))) 1934 c1 = max(0, min(c1, len(runes))) 1935 if c0 < c1 { 1936 intervals = append(intervals, [2]int{c0, c1}) 1937 } 1938 } 1939 intervals = mergeIntervals(intervals) 1940 1941 out[i] = ChunkLine{ 1942 Num: num, 1943 Spans: spanRunes(runes, intervals), 1944 Highlight: len(intervals) > 0, 1945 } 1946 } 1947 return out 1948} 1949 1950// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1951// Filename ranges live on line 1; columns are clamped to rune bounds. 1952func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1953 runes := []rune(name) 1954 var intervals [][2]int 1955 for _, rg := range ranges { 1956 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1957 continue 1958 } 1959 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1960 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1961 if c0 < c1 { 1962 intervals = append(intervals, [2]int{c0, c1}) 1963 } 1964 } 1965 return spanRunes(runes, mergeIntervals(intervals)) 1966} 1967 1968type CodeSearchParams struct { 1969 BaseParams 1970 FilterType string // "code" 1971 FilterQuery string 1972 Results []SearchResult 1973 Page pagination.Page 1974 HasMore bool 1975 ErrorMsg string 1976 1977 MatchCount int 1978 FileCount int 1979 TimeTaken time.Duration 1980} 1981 1982func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1983 params.FilterType = "code" 1984 return p.execute("search/search", w, params) 1985} 1986 1987func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1988 return p.execute("timeline/home", w, params) 1989} 1990 1991type CommentBodyFragmentParams struct { 1992 Comment models.Comment 1993 Reactions map[models.ReactionKind]models.ReactionDisplayData 1994 UserReacted map[models.ReactionKind]bool 1995} 1996 1997func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1998 return p.executePlain("fragments/comment/commentBody", w, params) 1999} 2000 2001type PullCommentFragmentParams struct { 2002 LoggedInUser *oauth.MultiAccountUser 2003 Comment models.Comment 2004 Reactions map[models.ReactionKind]models.ReactionDisplayData 2005 UserReacted map[models.ReactionKind]bool 2006 HxSwapOob bool 2007} 2008 2009func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error { 2010 return p.executePlain("fragments/comment/pullComment", w, params) 2011} 2012 2013type CommentHeaderFragmentParams struct { 2014 Comment models.Comment 2015 Reactions map[models.ReactionKind]models.ReactionDisplayData 2016 UserReacted map[models.ReactionKind]bool 2017 HxSwapOob bool 2018} 2019 2020func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 2021 return p.executePlain("fragments/comment/commentHeader", w, params) 2022} 2023 2024type EditCommentFragmentParams struct { 2025 Comment models.Comment 2026} 2027 2028func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 2029 return p.executePlain("fragments/comment/edit", w, params) 2030} 2031 2032type ReplyCommentFragmentParams struct { 2033 BaseParams 2034} 2035 2036func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 2037 return p.executePlain("fragments/comment/reply", w, params) 2038} 2039 2040type ReplyPlaceholderFragmentParams struct { 2041 BaseParams 2042} 2043 2044func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 2045 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 2046} 2047 2048func (p *Pages) Static() http.Handler { 2049 if p.dev { 2050 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 2051 } 2052 2053 sub, err := fs.Sub(p.embedFS, "static") 2054 if err != nil { 2055 p.logger.Error("no static dir found? that's crazy", "err", err) 2056 panic(err) 2057 } 2058 // Custom handler to apply Cache-Control headers for font files 2059 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 2060} 2061 2062func Cache(h http.Handler) http.Handler { 2063 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2064 path := strings.Split(r.URL.Path, "?")[0] 2065 2066 if strings.HasSuffix(path, ".css") { 2067 // on day for css files 2068 w.Header().Set("Cache-Control", "public, max-age=86400") 2069 } else { 2070 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 2071 } 2072 h.ServeHTTP(w, r) 2073 }) 2074} 2075 2076func (p *Pages) CssContentHash() string { 2077 cssFile, err := p.embedFS.Open("static/tw.css") 2078 if err != nil { 2079 slog.Debug("Error opening CSS file", "err", err) 2080 return "" 2081 } 2082 defer cssFile.Close() 2083 2084 hasher := sha256.New() 2085 if _, err := io.Copy(hasher, cssFile); err != nil { 2086 slog.Debug("Error hashing CSS file", "err", err) 2087 return "" 2088 } 2089 2090 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 2091} 2092 2093func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 2094 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 2095} 2096 2097func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 2098 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 2099} 2100 2101func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 2102 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 2103} 2104 2105func (p *Pages) Error500(w io.Writer) error { 2106 return p.execute("errors/500", w, nil) 2107} 2108 2109func (p *Pages) Error404(w io.Writer) error { 2110 return p.execute("errors/404", w, nil) 2111} 2112 2113func (p *Pages) ErrorKnot404(w io.Writer) error { 2114 return p.execute("errors/knot404", w, nil) 2115} 2116 2117func (p *Pages) Error503(w io.Writer) error { 2118 return p.execute("errors/503", w, nil) 2119}