Monorepo for Tangled tangled.org
1

Configure Feed

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

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