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