Monorepo for Tangled
0

Configure Feed

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

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