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
52 kB 2021 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 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1446 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1447 1448 LabelDefs map[string]*models.LabelDefinition 1449 VouchRelationships map[syntax.DID]*models.VouchRelationship 1450 VouchSkips map[syntax.DID]bool 1451} 1452 1453func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1454 params.Active = "pulls" 1455 return p.executeRepo("repo/pulls/pull", w, params) 1456} 1457 1458type PullResubmitParams struct { 1459 BaseParams 1460 RepoInfo repoinfo.RepoInfo 1461 Pull *models.Pull 1462 SubmissionId int 1463} 1464 1465func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1466 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1467} 1468 1469type PullActionsParams struct { 1470 BaseParams 1471 RepoInfo repoinfo.RepoInfo 1472 Pull *models.Pull 1473 RoundNumber int 1474 MergeCheck types.MergeCheckResponse 1475 ResubmitCheck ResubmitResult 1476 BranchDeleteStatus *models.BranchDeleteStatus 1477 Stack models.Stack 1478 1479 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1480 // that fetches the real, checked fragment 1481 Loading bool 1482} 1483 1484func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1485 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1486} 1487 1488type PullNewCommentParams struct { 1489 BaseParams 1490 RepoInfo repoinfo.RepoInfo 1491 Pull *models.Pull 1492 RoundNumber int 1493} 1494 1495func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1496 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1497} 1498 1499type RepoCompareParams struct { 1500 BaseParams 1501 RepoInfo repoinfo.RepoInfo 1502 Forks []models.Repo 1503 Branches []types.Branch 1504 Tags []*types.TagReference 1505 Base string 1506 Head string 1507 Diff *types.NiceDiff 1508 DiffOpts types.DiffOpts 1509 1510 Active string 1511} 1512 1513func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1514 params.Active = "overview" 1515 return p.executeRepo("repo/compare/compare", w, params) 1516} 1517 1518type RepoCompareNewParams struct { 1519 BaseParams 1520 RepoInfo repoinfo.RepoInfo 1521 Forks []models.Repo 1522 Branches []types.Branch 1523 Tags []*types.TagReference 1524 Base string 1525 Head string 1526 1527 Active string 1528} 1529 1530func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1531 params.Active = "overview" 1532 return p.executeRepo("repo/compare/new", w, params) 1533} 1534 1535type RepoCompareAllowPullParams struct { 1536 BaseParams 1537 RepoInfo repoinfo.RepoInfo 1538 Base string 1539 Head string 1540} 1541 1542func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1543 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1544} 1545 1546type RepoCompareDiffFragmentParams struct { 1547 Diff types.NiceDiff 1548 DiffOpts types.DiffOpts 1549} 1550 1551func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1552 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1553} 1554 1555type LabelPanelParams struct { 1556 BaseParams 1557 RepoInfo repoinfo.RepoInfo 1558 Defs map[string]*models.LabelDefinition 1559 Subject string 1560 State models.LabelState 1561} 1562 1563func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1564 return p.executePlain("repo/fragments/labelPanel", w, params) 1565} 1566 1567type EditLabelPanelParams struct { 1568 BaseParams 1569 RepoInfo repoinfo.RepoInfo 1570 Defs map[string]*models.LabelDefinition 1571 Subject string 1572 State models.LabelState 1573 Prefix string 1574} 1575 1576func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1577 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1578} 1579 1580type RepoStarsParams struct { 1581 BaseParams 1582 RepoInfo repoinfo.RepoInfo 1583 Active string 1584 Starrers []models.Star 1585 Page pagination.Page 1586 TotalCount int 1587} 1588 1589func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1590 params.Active = "overview" 1591 return p.executeRepo("repo/stars", w, params) 1592} 1593 1594type RepoForksParams struct { 1595 BaseParams 1596 RepoInfo repoinfo.RepoInfo 1597 Active string 1598 Forks []models.Repo 1599 Page pagination.Page 1600 TotalCount int 1601} 1602 1603func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1604 params.Active = "overview" 1605 return p.executeRepo("repo/forks", w, params) 1606} 1607 1608type PipelinesParams struct { 1609 BaseParams 1610 RepoInfo repoinfo.RepoInfo 1611 Pipelines []types.Pipeline 1612 Active string 1613 FilterKind string 1614 Total int64 1615} 1616 1617func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1618 params.Active = "pipelines" 1619 return p.executeRepo("repo/pipelines/pipelines", w, params) 1620} 1621 1622type LogBlockParams struct { 1623 Id int 1624 Name string 1625 Command string 1626 Collapsed bool 1627 StartTime time.Time 1628} 1629 1630func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1631 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1632} 1633 1634type LogBlockEndParams struct { 1635 Id int 1636 StartTime time.Time 1637 EndTime time.Time 1638} 1639 1640func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1641 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1642} 1643 1644type LogLineParams struct { 1645 Id int 1646 Content template.HTML 1647} 1648 1649func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1650 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1651} 1652 1653type WorkflowSymbolOOBParams struct { 1654 Name string 1655 Statuses models.WorkflowStatus 1656} 1657 1658func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1659 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1660} 1661 1662type WorkflowParams struct { 1663 BaseParams 1664 RepoInfo repoinfo.RepoInfo 1665 Pipeline types.Pipeline 1666 Workflow string 1667 LogUrl string 1668 Active string 1669} 1670 1671func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1672 params.Active = "pipelines" 1673 return p.executeRepo("repo/pipelines/workflow", w, params) 1674} 1675 1676type PutStringParams struct { 1677 BaseParams 1678 Action string 1679 1680 // this is supplied in the case of editing an existing string 1681 String models.String 1682} 1683 1684func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1685 return p.execute("strings/put", w, params) 1686} 1687 1688type StringsDashboardParams struct { 1689 BaseParams 1690 Card ProfileCard 1691 Strings []models.String 1692} 1693 1694func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1695 return p.execute("strings/dashboard", w, params) 1696} 1697 1698type StringTimelineParams struct { 1699 BaseParams 1700 Strings []models.String 1701} 1702 1703func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1704 return p.execute("strings/timeline", w, params) 1705} 1706 1707type SingleStringParams struct { 1708 BaseParams 1709 ShowRendered bool 1710 RenderToggle bool 1711 RenderedContents template.HTML 1712 String *models.String 1713 Stats models.StringStats 1714 IsStarred bool 1715 StarCount int 1716 Owner identity.Identity 1717 CommentList []models.CommentListItem 1718 1719 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1720 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1721 VouchRelationships map[syntax.DID]*models.VouchRelationship 1722} 1723 1724func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1725 return p.execute("strings/string", w, params) 1726} 1727 1728type SearchReposParams struct { 1729 BaseParams 1730 FilterType string // "repo" | "code" 1731 Repos []SearchResult 1732 Page pagination.Page 1733 ResultCount int 1734 FilterQuery string 1735 SortParam string 1736 TimeTaken time.Duration 1737 DocCount int64 1738 ErrorMsg string 1739} 1740 1741func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1742 params.FilterType = "repo" 1743 return p.execute("search/search", w, params) 1744} 1745 1746type SearchQuickParams struct { 1747 Repos []models.Repo 1748 Query string 1749 Total int 1750} 1751 1752func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1753 return p.executePlain("search/fragments/quick", w, params) 1754} 1755 1756func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1757 tpl, err := p.parse("search/fragments/quick") 1758 if err != nil { 1759 return err 1760 } 1761 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1762} 1763 1764type SearchResult struct { 1765 RepoDID syntax.DID 1766 Repo *models.Repo 1767 FilePath string 1768 Branches []string 1769 Commit string 1770 Language string 1771 1772 File *CodeSearchResult_File // filename match 1773 Chunks CodeSearchResult_Chunks // content matches 1774} 1775 1776// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1777type CodeSearchResult_Chunk struct { 1778 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1779 MatchCount int // number of match ranges in this chunk 1780} 1781 1782type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1783 1784func (cs CodeSearchResult_Chunks) MatchCount() int { 1785 count := 0 1786 for _, c := range cs { 1787 count += c.MatchCount 1788 } 1789 return count 1790} 1791 1792type CodeSearchResult_File struct { 1793 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1794} 1795 1796type ChunkSpan struct { 1797 Text string 1798 Match bool 1799} 1800 1801type ChunkLine struct { 1802 Num int 1803 Spans []ChunkSpan 1804 Highlight bool 1805} 1806 1807// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1808// line into matched/unmatched spans using ranges. startLine is the 1-based line 1809// number of the first line. 1810func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1811 if startLine < 1 { 1812 startLine = 1 1813 } 1814 // trim a single trailing newline so we don't emit a spurious empty line 1815 content = strings.TrimSuffix(content, "\n") 1816 lines := strings.Split(content, "\n") 1817 out := make([]ChunkLine, len(lines)) 1818 for i, text := range lines { 1819 num := startLine + i 1820 runes := []rune(text) 1821 1822 // collect matched rune intervals [c0,c1) for this line 1823 var intervals [][2]int 1824 for _, rg := range ranges { 1825 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1826 continue 1827 } 1828 c0, c1 := 0, len(runes) 1829 if num == int(rg.Start.LineNumber) { 1830 c0 = int(rg.Start.Column) - 1 1831 } 1832 if num == int(rg.End.LineNumber) { 1833 c1 = int(rg.End.Column) - 1 1834 } 1835 c0 = max(0, min(c0, len(runes))) 1836 c1 = max(0, min(c1, len(runes))) 1837 if c0 < c1 { 1838 intervals = append(intervals, [2]int{c0, c1}) 1839 } 1840 } 1841 intervals = mergeIntervals(intervals) 1842 1843 out[i] = ChunkLine{ 1844 Num: num, 1845 Spans: spanRunes(runes, intervals), 1846 Highlight: len(intervals) > 0, 1847 } 1848 } 1849 return out 1850} 1851 1852// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1853// Filename ranges live on line 1; columns are clamped to rune bounds. 1854func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1855 runes := []rune(name) 1856 var intervals [][2]int 1857 for _, rg := range ranges { 1858 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1859 continue 1860 } 1861 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1862 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1863 if c0 < c1 { 1864 intervals = append(intervals, [2]int{c0, c1}) 1865 } 1866 } 1867 return spanRunes(runes, mergeIntervals(intervals)) 1868} 1869 1870type CodeSearchParams struct { 1871 BaseParams 1872 FilterType string // "code" 1873 FilterQuery string 1874 Results []SearchResult 1875 Page pagination.Page 1876 HasMore bool 1877 ErrorMsg string 1878 1879 MatchCount int 1880 FileCount int 1881 TimeTaken time.Duration 1882} 1883 1884func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1885 params.FilterType = "code" 1886 return p.execute("search/search", w, params) 1887} 1888 1889func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1890 return p.execute("timeline/home", w, params) 1891} 1892 1893type CommentBodyFragmentParams struct { 1894 Comment models.Comment 1895 Reactions map[models.ReactionKind]models.ReactionDisplayData 1896 UserReacted map[models.ReactionKind]bool 1897} 1898 1899func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1900 return p.executePlain("fragments/comment/commentBody", w, params) 1901} 1902 1903type PullCommentFragmentParams struct { 1904 LoggedInUser *oauth.MultiAccountUser 1905 Comment models.Comment 1906 Reactions map[models.ReactionKind]models.ReactionDisplayData 1907 UserReacted map[models.ReactionKind]bool 1908 HxSwapOob bool 1909} 1910 1911func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error { 1912 return p.executePlain("fragments/comment/pullComment", w, params) 1913} 1914 1915type CommentHeaderFragmentParams struct { 1916 Comment models.Comment 1917 Reactions map[models.ReactionKind]models.ReactionDisplayData 1918 UserReacted map[models.ReactionKind]bool 1919 HxSwapOob bool 1920} 1921 1922func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 1923 return p.executePlain("fragments/comment/commentHeader", w, params) 1924} 1925 1926type EditCommentFragmentParams struct { 1927 Comment models.Comment 1928} 1929 1930func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1931 return p.executePlain("fragments/comment/edit", w, params) 1932} 1933 1934type ReplyCommentFragmentParams struct { 1935 BaseParams 1936} 1937 1938func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1939 return p.executePlain("fragments/comment/reply", w, params) 1940} 1941 1942type ReplyPlaceholderFragmentParams struct { 1943 BaseParams 1944} 1945 1946func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1947 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1948} 1949 1950func (p *Pages) Static() http.Handler { 1951 if p.dev { 1952 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1953 } 1954 1955 sub, err := fs.Sub(p.embedFS, "static") 1956 if err != nil { 1957 p.logger.Error("no static dir found? that's crazy", "err", err) 1958 panic(err) 1959 } 1960 // Custom handler to apply Cache-Control headers for font files 1961 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1962} 1963 1964func Cache(h http.Handler) http.Handler { 1965 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1966 path := strings.Split(r.URL.Path, "?")[0] 1967 1968 if strings.HasSuffix(path, ".css") { 1969 // on day for css files 1970 w.Header().Set("Cache-Control", "public, max-age=86400") 1971 } else { 1972 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1973 } 1974 h.ServeHTTP(w, r) 1975 }) 1976} 1977 1978func (p *Pages) CssContentHash() string { 1979 cssFile, err := p.embedFS.Open("static/tw.css") 1980 if err != nil { 1981 slog.Debug("Error opening CSS file", "err", err) 1982 return "" 1983 } 1984 defer cssFile.Close() 1985 1986 hasher := sha256.New() 1987 if _, err := io.Copy(hasher, cssFile); err != nil { 1988 slog.Debug("Error hashing CSS file", "err", err) 1989 return "" 1990 } 1991 1992 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1993} 1994 1995func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1996 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1997} 1998 1999func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 2000 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 2001} 2002 2003func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 2004 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 2005} 2006 2007func (p *Pages) Error500(w io.Writer) error { 2008 return p.execute("errors/500", w, nil) 2009} 2010 2011func (p *Pages) Error404(w io.Writer) error { 2012 return p.execute("errors/404", w, nil) 2013} 2014 2015func (p *Pages) ErrorKnot404(w io.Writer) error { 2016 return p.execute("errors/knot404", w, nil) 2017} 2018 2019func (p *Pages) Error503(w io.Writer) error { 2020 return p.execute("errors/503", w, nil) 2021}