This repository has no description
0

Configure Feed

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

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