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