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 2001 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 ProfileScript string 685} 686 687type ProfileStats struct { 688 RepoCount int64 689 StarredCount int64 690 StringCount int64 691 FollowersCount int64 692 FollowingCount int64 693} 694 695func (p *ProfileCard) GetTabs() [][]any { 696 tabs := [][]any{ 697 {"overview", "overview", "square-chart-gantt", nil}, 698 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 699 {"starred", "starred", "star", p.Stats.StarredCount}, 700 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 701 {"vouches", "vouches", "shield", nil}, 702 } 703 704 return tabs 705} 706 707type ProfileOverviewParams struct { 708 BaseParams 709 Repos []models.Repo 710 CollaboratingRepos []models.Repo 711 ProfileTimeline *models.ProfileTimeline 712 Card *ProfileCard 713 Active string 714 ShowPunchcard bool 715} 716 717func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 718 params.Active = "overview" 719 return p.executeProfile("user/overview", w, params) 720} 721 722type ProfileReposParams struct { 723 BaseParams 724 Repos []models.Repo 725 StarStatuses map[string]bool 726 Card *ProfileCard 727 Active string 728 Page pagination.Page 729 RepoCount int 730 FilterQuery string 731} 732 733func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 734 params.Active = "repos" 735 return p.executeProfile("user/repos", w, params) 736} 737 738type ProfileStarredParams struct { 739 BaseParams 740 Repos []models.Repo 741 Card *ProfileCard 742 Page pagination.Page 743 Total int 744 Active string 745} 746 747func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 748 params.Active = "starred" 749 return p.executeProfile("user/starred", w, params) 750} 751 752type ProfileStringsParams struct { 753 BaseParams 754 Strings []models.String 755 Card *ProfileCard 756 Active string 757} 758 759func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 760 params.Active = "strings" 761 return p.executeProfile("user/strings", w, params) 762} 763 764type ProfileVouchesParams struct { 765 BaseParams 766 Vouches []models.Vouch 767 Suggestions []models.VouchSuggestion 768 Card *ProfileCard 769 Page pagination.Page 770 VouchCount int 771 Active string 772 EvidencePulls map[syntax.ATURI]*models.Pull 773 EvidenceIssues map[syntax.ATURI]*models.Issue 774} 775 776func (p *Pages) ProfileVouches(w io.Writer, params ProfileVouchesParams) error { 777 params.Active = "vouches" 778 return p.executeProfile("user/vouches", w, params) 779} 780 781type FollowCard struct { 782 UserDid string 783 BaseParams 784 FollowStatus models.FollowStatus 785 FollowersCount int64 786 FollowingCount int64 787 Profile *models.Profile 788} 789 790type ProfileFollowersParams struct { 791 BaseParams 792 Followers []FollowCard 793 Card *ProfileCard 794 Active string 795} 796 797func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 798 params.Active = "overview" 799 return p.executeProfile("user/followers", w, params) 800} 801 802type ProfileFollowingParams struct { 803 BaseParams 804 Following []FollowCard 805 Card *ProfileCard 806 Active string 807} 808 809func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 810 params.Active = "overview" 811 return p.executeProfile("user/following", w, params) 812} 813 814type FollowFragmentParams struct { 815 UserDid string 816 FollowStatus models.FollowStatus 817 FollowersCount int64 818} 819 820func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 821 return p.executePlain("user/fragments/follow-oob", w, params) 822} 823 824type ProfilePopoverParams struct { 825 BaseParams 826 UserDid string 827 Profile *models.Profile 828 FollowStatus models.FollowStatus 829 VouchRelationship *models.VouchRelationship 830 Stats ProfilePopoverStats 831} 832 833type ProfilePopoverStats struct { 834 FollowersCount int64 835 FollowingCount int64 836} 837 838func (p *Pages) ProfilePopoverFragment(w io.Writer, params ProfilePopoverParams) error { 839 return p.executePlain("user/fragments/profilePopover", w, params) 840} 841 842type EditBioParams struct { 843 BaseParams 844 Profile *models.Profile 845 AlsoKnownAs []string 846} 847 848func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 849 return p.executePlain("user/fragments/editBio", w, params) 850} 851 852type EditPinsParams struct { 853 BaseParams 854 Profile *models.Profile 855 AllRepos []PinnedRepo 856} 857 858type PinnedRepo struct { 859 IsPinned bool 860 models.Repo 861} 862 863func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 864 return p.executePlain("user/fragments/editPins", w, params) 865} 866 867type StarBtnFragmentParams struct { 868 IsStarred bool 869 SubjectAt syntax.ATURI 870 StarCount int 871 RepoName string 872 HxSwapOob bool 873} 874 875func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 876 params.HxSwapOob = true 877 return p.executePlain("fragments/starBtn", w, params) 878} 879 880type RepoIndexParams struct { 881 BaseParams 882 RepoInfo repoinfo.RepoInfo 883 Active string 884 TagMap map[string][]string 885 CommitsTrunc []types.Commit 886 TagsTrunc []*types.TagReference 887 BranchesTrunc []types.Branch 888 // ForkInfo *types.ForkInfo 889 HTMLReadme template.HTML 890 Raw bool 891 EmailToDid map[string]string 892 VerifiedCommits commitverify.VerifiedCommits 893 Languages []types.RepoLanguageDetails 894 Pipelines map[string]models.Pipeline 895 NeedsKnotUpgrade bool 896 KnotUnreachable bool 897 types.RepoIndexResponse 898} 899 900func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 901 params.Active = "overview" 902 if params.IsEmpty { 903 return p.executeRepo("repo/empty", w, params) 904 } 905 906 if params.NeedsKnotUpgrade { 907 return p.executeRepo("repo/needsUpgrade", w, params) 908 } 909 910 if params.KnotUnreachable { 911 return p.executeRepo("repo/knotUnreachable", w, params) 912 } 913 914 rctx := p.rctx.Clone() 915 rctx.RepoInfo = params.RepoInfo 916 rctx.RepoInfo.Ref = params.Ref 917 rctx.RendererType = markup.RendererTypeRepoMarkdown 918 919 if params.ReadmeFileName != "" { 920 switch markup.GetFormat(params.ReadmeFileName) { 921 case markup.FormatMarkdown: 922 params.Raw = false 923 htmlString := rctx.RenderMarkdown(params.Readme) 924 sanitized := sanitizer.SanitizeDefault(htmlString) 925 params.HTMLReadme = template.HTML(sanitized) 926 default: 927 params.Raw = true 928 } 929 } 930 931 return p.executeRepo("repo/index", w, params) 932} 933 934type RepoSearchParams struct { 935 BaseParams 936 RepoInfo repoinfo.RepoInfo 937 Active string 938 FilterQuery string 939} 940 941func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 942 params.Active = "overview" 943 return p.executeRepo("repo/search", w, params) 944} 945 946type RepoSearchResultsFragmentParams struct { 947 Query string 948 Results []SearchResult 949 ErrorMsg string 950} 951 952func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 953 return p.executePlain("repo/fragments/searchResults", w, params) 954} 955 956type RepoLogParams struct { 957 BaseParams 958 RepoInfo repoinfo.RepoInfo 959 TagMap map[string][]string 960 Active string 961 EmailToDid map[string]string 962 VerifiedCommits commitverify.VerifiedCommits 963 Pipelines map[string]models.Pipeline 964 965 types.RepoLogResponse 966} 967 968func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 969 params.Active = "overview" 970 return p.executeRepo("repo/log", w, params) 971} 972 973type RepoCommitParams struct { 974 BaseParams 975 RepoInfo repoinfo.RepoInfo 976 Active string 977 EmailToDid map[string]string 978 Pipeline *models.Pipeline 979 DiffOpts types.DiffOpts 980 981 // singular because it's always going to be just one 982 VerifiedCommit commitverify.VerifiedCommits 983 984 types.RepoCommitResponse 985} 986 987func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 988 params.Active = "overview" 989 return p.executeRepo("repo/commit", w, params) 990} 991 992type RepoTreeParams struct { 993 BaseParams 994 RepoInfo repoinfo.RepoInfo 995 Active string 996 BreadCrumbs [][]string 997 Path string 998 Raw bool 999 HTMLReadme template.HTML 1000 EmailToDid map[string]string 1001 LastCommitInfo *types.LastCommitInfo 1002 Ref string 1003 Parent string 1004 DotDot string 1005 Files []types.NiceTree 1006 ReadmeFileName string 1007 Readme string 1008} 1009 1010type RepoTreeStats struct { 1011 NumFolders uint64 1012 NumFiles uint64 1013} 1014 1015func (r RepoTreeParams) TreeStats() RepoTreeStats { 1016 numFolders, numFiles := 0, 0 1017 for _, f := range r.Files { 1018 if !f.IsFile() { 1019 numFolders += 1 1020 } else if f.IsFile() { 1021 numFiles += 1 1022 } 1023 } 1024 1025 return RepoTreeStats{ 1026 NumFolders: uint64(numFolders), 1027 NumFiles: uint64(numFiles), 1028 } 1029} 1030 1031func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 1032 params.Active = "overview" 1033 1034 rctx := p.rctx.Clone() 1035 rctx.RepoInfo = params.RepoInfo 1036 rctx.RepoInfo.Ref = params.Ref 1037 rctx.RendererType = markup.RendererTypeRepoMarkdown 1038 1039 if params.ReadmeFileName != "" { 1040 switch markup.GetFormat(params.ReadmeFileName) { 1041 case markup.FormatMarkdown: 1042 params.Raw = false 1043 htmlString := rctx.RenderMarkdown(params.Readme) 1044 sanitized := sanitizer.SanitizeDefault(htmlString) 1045 params.HTMLReadme = template.HTML(sanitized) 1046 default: 1047 params.Raw = true 1048 } 1049 } 1050 1051 return p.executeRepo("repo/tree", w, params) 1052} 1053 1054type RepoBranchesParams struct { 1055 BaseParams 1056 RepoInfo repoinfo.RepoInfo 1057 Active string 1058 types.RepoBranchesResponse 1059} 1060 1061func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 1062 params.Active = "overview" 1063 return p.executeRepo("repo/branches", w, params) 1064} 1065 1066type RepoTagsParams struct { 1067 BaseParams 1068 RepoInfo repoinfo.RepoInfo 1069 Active string 1070 types.RepoTagsResponse 1071 ArtifactMap map[plumbing.Hash][]models.Artifact 1072 DanglingArtifacts []models.Artifact 1073} 1074 1075func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 1076 params.Active = "overview" 1077 return p.executeRepo("repo/tags", w, params) 1078} 1079 1080type RepoTagParams struct { 1081 BaseParams 1082 RepoInfo repoinfo.RepoInfo 1083 Active string 1084 types.RepoTagResponse 1085 ArtifactMap map[plumbing.Hash][]models.Artifact 1086 DanglingArtifacts []models.Artifact 1087} 1088 1089func (p *Pages) RepoTag(w io.Writer, params RepoTagParams) error { 1090 params.Active = "overview" 1091 return p.executeRepo("repo/tag", w, params) 1092} 1093 1094type RepoArtifactParams struct { 1095 BaseParams 1096 RepoInfo repoinfo.RepoInfo 1097 Artifact models.Artifact 1098} 1099 1100func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 1101 return p.executePlain("repo/fragments/artifact", w, params) 1102} 1103 1104type RepoBlobParams struct { 1105 BaseParams 1106 RepoInfo repoinfo.RepoInfo 1107 Active string // always "overview" 1108 BreadCrumbs [][]string 1109 BlobView models.BlobView // TODO: expose this struct 1110 ShowRendered bool 1111 EmailToDid map[string]string 1112 LastCommitInfo *types.LastCommitInfo 1113 Ref string 1114 Path string 1115 Language string 1116} 1117 1118func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 1119 params.Active = "overview" 1120 return p.executeRepo("repo/blob", w, params) 1121} 1122 1123type Collaborator struct { 1124 Did string 1125 Role string 1126} 1127 1128type RepoSettingsParams struct { 1129 BaseParams 1130 RepoInfo repoinfo.RepoInfo 1131 Collaborators []Collaborator 1132 Active string 1133 Branches []types.Branch 1134 Spindles []string 1135 CurrentSpindle string 1136 Secrets []*tangled.RepoListSecrets_Secret 1137 1138 // TODO: use repoinfo.roles 1139 IsCollaboratorInviteAllowed bool 1140} 1141 1142func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 1143 params.Active = "settings" 1144 return p.executeRepo("repo/settings", w, params) 1145} 1146 1147type RepoGeneralSettingsParams struct { 1148 BaseParams 1149 RepoInfo repoinfo.RepoInfo 1150 Labels []models.LabelDefinition 1151 DefaultLabels []models.LabelDefinition 1152 SubscribedLabels map[string]struct{} 1153 ShouldSubscribeAll bool 1154 Active string 1155 Tab string 1156 Branches []types.Branch 1157} 1158 1159func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 1160 params.Active = "settings" 1161 params.Tab = "general" 1162 return p.executeRepo("repo/settings/general", w, params) 1163} 1164 1165type RepoAccessSettingsParams struct { 1166 BaseParams 1167 RepoInfo repoinfo.RepoInfo 1168 Active string 1169 Tab string 1170 Collaborators []Collaborator 1171 CanRemoveCollaborator bool 1172} 1173 1174func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 1175 params.Active = "settings" 1176 params.Tab = "access" 1177 return p.executeRepo("repo/settings/access", w, params) 1178} 1179 1180type RepoPipelineSettingsParams struct { 1181 BaseParams 1182 RepoInfo repoinfo.RepoInfo 1183 Active string 1184 Tab string 1185 Spindles []string 1186 CurrentSpindle string 1187 Secrets []map[string]any 1188} 1189 1190func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 1191 params.Active = "settings" 1192 params.Tab = "pipelines" 1193 return p.executeRepo("repo/settings/pipelines", w, params) 1194} 1195 1196type RepoWebhooksSettingsParams struct { 1197 BaseParams 1198 RepoInfo repoinfo.RepoInfo 1199 Active string 1200 Tab string 1201 Webhooks []models.Webhook 1202 WebhookDeliveries map[int64][]models.WebhookDelivery 1203} 1204 1205func (p *Pages) RepoWebhooksSettings(w io.Writer, params RepoWebhooksSettingsParams) error { 1206 params.Active = "settings" 1207 params.Tab = "hooks" 1208 return p.executeRepo("repo/settings/hooks", w, params) 1209} 1210 1211type WebhookDeliveriesListParams struct { 1212 BaseParams 1213 RepoInfo repoinfo.RepoInfo 1214 Webhook *models.Webhook 1215 Deliveries []models.WebhookDelivery 1216} 1217 1218func (p *Pages) WebhookDeliveriesList(w io.Writer, params WebhookDeliveriesListParams) error { 1219 tpl, err := p.parse("repo/settings/fragments/webhookDeliveries") 1220 if err != nil { 1221 return err 1222 } 1223 return tpl.ExecuteTemplate(w, "repo/settings/fragments/webhookDeliveries", params) 1224} 1225 1226type RepoSiteSettingsParams struct { 1227 BaseParams 1228 RepoInfo repoinfo.RepoInfo 1229 Active string 1230 Tab string 1231 Branches []types.Branch 1232 SiteConfig *models.RepoSite 1233 OwnerClaim *models.DomainClaim 1234 Deploys []models.SiteDeploy 1235 IndexSiteTakenBy string // repo_at of another repo that already holds is_index, or "" 1236} 1237 1238func (p *Pages) RepoSiteSettings(w io.Writer, params RepoSiteSettingsParams) error { 1239 params.Active = "settings" 1240 params.Tab = "sites" 1241 return p.executeRepo("repo/settings/sites", w, params) 1242} 1243 1244type RepoIssuesParams struct { 1245 BaseParams 1246 RepoInfo repoinfo.RepoInfo 1247 Active string 1248 Issues []models.Issue 1249 IssueCount int 1250 LabelDefs map[string]*models.LabelDefinition 1251 Page pagination.Page 1252 FilterState string 1253 FilterQuery string 1254 BaseFilterQuery string 1255 VouchRelationships map[syntax.DID]*models.VouchRelationship 1256} 1257 1258func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 1259 params.Active = "issues" 1260 return p.executeRepo("repo/issues/issues", w, params) 1261} 1262 1263type RepoSingleIssueParams struct { 1264 BaseParams 1265 RepoInfo repoinfo.RepoInfo 1266 Active string 1267 Issue *models.Issue 1268 CommentList []models.CommentListItem 1269 Backlinks []models.RichReferenceLink 1270 LabelDefs map[string]*models.LabelDefinition 1271 1272 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1273 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1274 VouchRelationships map[syntax.DID]*models.VouchRelationship 1275} 1276 1277func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 1278 params.Active = "issues" 1279 return p.executeRepo("repo/issues/issue", w, params) 1280} 1281 1282type EditIssueParams struct { 1283 BaseParams 1284 RepoInfo repoinfo.RepoInfo 1285 Issue *models.Issue 1286 Action string 1287} 1288 1289func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 1290 params.Action = "edit" 1291 return p.executePlain("repo/issues/fragments/putIssue", w, params) 1292} 1293 1294type ThreadReactionFragmentParams struct { 1295 Kind models.ReactionKind 1296 Count int 1297 Users []string 1298 IsReacted bool 1299 CommentRkey string 1300 SubjectUri string 1301} 1302 1303func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 1304 return p.executePlain("repo/fragments/reaction", w, params) 1305} 1306 1307type RepoNewIssueParams struct { 1308 BaseParams 1309 RepoInfo repoinfo.RepoInfo 1310 Issue *models.Issue // existing issue if any -- passed when editing 1311 Active string 1312 Action string 1313} 1314 1315func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 1316 params.Active = "issues" 1317 params.Action = "create" 1318 return p.executeRepo("repo/issues/new", w, params) 1319} 1320 1321type StackedDiff struct { 1322 Diff *types.NiceDiff 1323 Opts types.DiffOpts 1324} 1325 1326type RepoNewPullParams struct { 1327 BaseParams 1328 RepoInfo repoinfo.RepoInfo 1329 Branches []types.Branch 1330 SourceBranches []types.Branch 1331 ForkBranches []types.Branch 1332 Forks []models.Repo 1333 Source Source 1334 SourceBranch string 1335 TargetBranch string 1336 Fork string 1337 Patch string 1338 Title string 1339 Body string 1340 TitleDirty bool 1341 BodyDirty bool 1342 IsStacked bool 1343 Comparison *types.RepoFormatPatchResponse 1344 Diff *types.NiceDiff 1345 DiffOpts types.DiffOpts 1346 StackedDiffs []StackedDiff 1347 MergeCheck *types.MergeCheckResponse 1348 StackTitles map[string]string 1349 StackBodies map[string]string 1350 PrefillError string 1351 Active string 1352 LabelDefs map[string]*models.LabelDefinition 1353 LabelState models.LabelState 1354 StackLabelStates map[string]models.LabelState 1355} 1356 1357func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1358 params.Active = "pulls" 1359 return p.executeRepo("repo/pulls/new", w, params) 1360} 1361 1362func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1363 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1364} 1365 1366func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1367 return p.executePlain("fragments/markdownPreview", w, body) 1368} 1369 1370type RepoPullsParams struct { 1371 BaseParams 1372 RepoInfo repoinfo.RepoInfo 1373 Pulls []*models.Pull 1374 Active string 1375 FilterState string 1376 FilterQuery string 1377 BaseFilterQuery string 1378 Stacks []models.Stack 1379 Pipelines map[string]models.Pipeline 1380 LabelDefs map[string]*models.LabelDefinition 1381 Page pagination.Page 1382 PullCount int 1383 VouchRelationships map[syntax.DID]*models.VouchRelationship 1384} 1385 1386func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1387 params.Active = "pulls" 1388 return p.executeRepo("repo/pulls/pulls", w, params) 1389} 1390 1391type ResubmitResult uint64 1392 1393const ( 1394 ShouldResubmit ResubmitResult = iota 1395 ShouldNotResubmit 1396 Unknown 1397) 1398 1399func (r ResubmitResult) Yes() bool { 1400 return r == ShouldResubmit 1401} 1402func (r ResubmitResult) No() bool { 1403 return r == ShouldNotResubmit 1404} 1405func (r ResubmitResult) Unknown() bool { 1406 return r == Unknown 1407} 1408 1409type RepoSinglePullParams struct { 1410 BaseParams 1411 RepoInfo repoinfo.RepoInfo 1412 Active string 1413 Pull *models.Pull 1414 Stack models.Stack 1415 Backlinks []models.RichReferenceLink 1416 BranchDeleteStatus *models.BranchDeleteStatus 1417 MergeCheck types.MergeCheckResponse 1418 ResubmitCheck ResubmitResult 1419 Pipelines map[string]models.Pipeline 1420 Diff types.DiffRenderer 1421 DiffOpts types.DiffOpts 1422 ActiveRound int 1423 IsInterdiff bool 1424 1425 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1426 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1427 1428 LabelDefs map[string]*models.LabelDefinition 1429 VouchRelationships map[syntax.DID]*models.VouchRelationship 1430 VouchSkips map[syntax.DID]bool 1431} 1432 1433func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1434 params.Active = "pulls" 1435 return p.executeRepo("repo/pulls/pull", w, params) 1436} 1437 1438type PullResubmitParams struct { 1439 BaseParams 1440 RepoInfo repoinfo.RepoInfo 1441 Pull *models.Pull 1442 SubmissionId int 1443} 1444 1445func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1446 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1447} 1448 1449type PullActionsParams struct { 1450 BaseParams 1451 RepoInfo repoinfo.RepoInfo 1452 Pull *models.Pull 1453 RoundNumber int 1454 MergeCheck types.MergeCheckResponse 1455 ResubmitCheck ResubmitResult 1456 BranchDeleteStatus *models.BranchDeleteStatus 1457 Stack models.Stack 1458 1459 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1460 // that fetches the real, checked fragment 1461 Loading bool 1462} 1463 1464func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1465 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1466} 1467 1468type PullNewCommentParams struct { 1469 BaseParams 1470 RepoInfo repoinfo.RepoInfo 1471 Pull *models.Pull 1472 RoundNumber int 1473} 1474 1475func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1476 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1477} 1478 1479type RepoCompareParams struct { 1480 BaseParams 1481 RepoInfo repoinfo.RepoInfo 1482 Forks []models.Repo 1483 Branches []types.Branch 1484 Tags []*types.TagReference 1485 Base string 1486 Head string 1487 Diff *types.NiceDiff 1488 DiffOpts types.DiffOpts 1489 1490 Active string 1491} 1492 1493func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1494 params.Active = "overview" 1495 return p.executeRepo("repo/compare/compare", w, params) 1496} 1497 1498type RepoCompareNewParams struct { 1499 BaseParams 1500 RepoInfo repoinfo.RepoInfo 1501 Forks []models.Repo 1502 Branches []types.Branch 1503 Tags []*types.TagReference 1504 Base string 1505 Head string 1506 1507 Active string 1508} 1509 1510func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1511 params.Active = "overview" 1512 return p.executeRepo("repo/compare/new", w, params) 1513} 1514 1515type RepoCompareAllowPullParams struct { 1516 BaseParams 1517 RepoInfo repoinfo.RepoInfo 1518 Base string 1519 Head string 1520} 1521 1522func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1523 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1524} 1525 1526type RepoCompareDiffFragmentParams struct { 1527 Diff types.NiceDiff 1528 DiffOpts types.DiffOpts 1529} 1530 1531func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1532 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1533} 1534 1535type LabelPanelParams struct { 1536 BaseParams 1537 RepoInfo repoinfo.RepoInfo 1538 Defs map[string]*models.LabelDefinition 1539 Subject string 1540 State models.LabelState 1541} 1542 1543func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1544 return p.executePlain("repo/fragments/labelPanel", w, params) 1545} 1546 1547type EditLabelPanelParams struct { 1548 BaseParams 1549 RepoInfo repoinfo.RepoInfo 1550 Defs map[string]*models.LabelDefinition 1551 Subject string 1552 State models.LabelState 1553 Prefix string 1554} 1555 1556func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1557 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1558} 1559 1560type RepoStarsParams struct { 1561 BaseParams 1562 RepoInfo repoinfo.RepoInfo 1563 Active string 1564 Starrers []models.Star 1565 Page pagination.Page 1566 TotalCount int 1567} 1568 1569func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1570 params.Active = "overview" 1571 return p.executeRepo("repo/stars", w, params) 1572} 1573 1574type RepoForksParams struct { 1575 BaseParams 1576 RepoInfo repoinfo.RepoInfo 1577 Active string 1578 Forks []models.Repo 1579 Page pagination.Page 1580 TotalCount int 1581} 1582 1583func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1584 params.Active = "overview" 1585 return p.executeRepo("repo/forks", w, params) 1586} 1587 1588type PipelinesParams struct { 1589 BaseParams 1590 RepoInfo repoinfo.RepoInfo 1591 Pipelines []models.Pipeline 1592 Active string 1593 FilterKind string 1594 Total int64 1595} 1596 1597func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1598 params.Active = "pipelines" 1599 return p.executeRepo("repo/pipelines/pipelines", w, params) 1600} 1601 1602type LogBlockParams struct { 1603 Id int 1604 Name string 1605 Command string 1606 Collapsed bool 1607 StartTime time.Time 1608} 1609 1610func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1611 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1612} 1613 1614type LogBlockEndParams struct { 1615 Id int 1616 StartTime time.Time 1617 EndTime time.Time 1618} 1619 1620func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1621 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1622} 1623 1624type LogLineParams struct { 1625 Id int 1626 Content template.HTML 1627} 1628 1629func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1630 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1631} 1632 1633type WorkflowSymbolOOBParams struct { 1634 Name string 1635 Statuses models.WorkflowStatus 1636} 1637 1638func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1639 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1640} 1641 1642type WorkflowParams struct { 1643 BaseParams 1644 RepoInfo repoinfo.RepoInfo 1645 Pipeline models.Pipeline 1646 Workflow string 1647 LogUrl string 1648 Active string 1649} 1650 1651func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1652 params.Active = "pipelines" 1653 return p.executeRepo("repo/pipelines/workflow", w, params) 1654} 1655 1656type PutStringParams struct { 1657 BaseParams 1658 Action string 1659 1660 // this is supplied in the case of editing an existing string 1661 String models.String 1662} 1663 1664func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1665 return p.execute("strings/put", w, params) 1666} 1667 1668type StringsDashboardParams struct { 1669 BaseParams 1670 Card ProfileCard 1671 Strings []models.String 1672} 1673 1674func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1675 return p.execute("strings/dashboard", w, params) 1676} 1677 1678type StringTimelineParams struct { 1679 BaseParams 1680 Strings []models.String 1681} 1682 1683func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1684 return p.execute("strings/timeline", w, params) 1685} 1686 1687type SingleStringParams struct { 1688 BaseParams 1689 ShowRendered bool 1690 RenderToggle bool 1691 RenderedContents template.HTML 1692 String *models.String 1693 Stats models.StringStats 1694 IsStarred bool 1695 StarCount int 1696 Owner identity.Identity 1697 CommentList []models.CommentListItem 1698 1699 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1700 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1701 VouchRelationships map[syntax.DID]*models.VouchRelationship 1702} 1703 1704func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1705 return p.execute("strings/string", w, params) 1706} 1707 1708type SearchReposParams struct { 1709 BaseParams 1710 FilterType string // "repo" | "code" 1711 Repos []SearchResult 1712 Page pagination.Page 1713 ResultCount int 1714 FilterQuery string 1715 SortParam string 1716 TimeTaken time.Duration 1717 DocCount int64 1718 ErrorMsg string 1719} 1720 1721func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1722 params.FilterType = "repo" 1723 return p.execute("search/search", w, params) 1724} 1725 1726type SearchQuickParams struct { 1727 Repos []models.Repo 1728 Query string 1729 Total int 1730} 1731 1732func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1733 return p.executePlain("search/fragments/quick", w, params) 1734} 1735 1736func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1737 tpl, err := p.parse("search/fragments/quick") 1738 if err != nil { 1739 return err 1740 } 1741 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1742} 1743 1744type SearchResult struct { 1745 RepoDID syntax.DID 1746 Repo *models.Repo 1747 FilePath string 1748 Branches []string 1749 Commit string 1750 Language string 1751 1752 File *CodeSearchResult_File // filename match 1753 Chunks CodeSearchResult_Chunks // content matches 1754} 1755 1756// CodeSearchResult_Chunk is a content match with its lines pre-rendered. 1757type CodeSearchResult_Chunk struct { 1758 Lines []ChunkLine // precomputed from Content/ContentStartLine/Ranges 1759 MatchCount int // number of match ranges in this chunk 1760} 1761 1762type CodeSearchResult_Chunks []CodeSearchResult_Chunk 1763 1764func (cs CodeSearchResult_Chunks) MatchCount() int { 1765 count := 0 1766 for _, c := range cs { 1767 count += c.MatchCount 1768 } 1769 return count 1770} 1771 1772type CodeSearchResult_File struct { 1773 NameSpans []ChunkSpan // precomputed from FilePath/Ranges 1774} 1775 1776type ChunkSpan struct { 1777 Text string 1778 Match bool 1779} 1780 1781type ChunkLine struct { 1782 Num int 1783 Spans []ChunkSpan 1784 Highlight bool 1785} 1786 1787// ChunkLines renders a chunk's Content into per-line ChunkLines, splitting each 1788// line into matched/unmatched spans using ranges. startLine is the 1-based line 1789// number of the first line. 1790func ChunkLines(content string, startLine int, ranges []zoekt.Range) []ChunkLine { 1791 if startLine < 1 { 1792 startLine = 1 1793 } 1794 // trim a single trailing newline so we don't emit a spurious empty line 1795 content = strings.TrimSuffix(content, "\n") 1796 lines := strings.Split(content, "\n") 1797 out := make([]ChunkLine, len(lines)) 1798 for i, text := range lines { 1799 num := startLine + i 1800 runes := []rune(text) 1801 1802 // collect matched rune intervals [c0,c1) for this line 1803 var intervals [][2]int 1804 for _, rg := range ranges { 1805 if num < int(rg.Start.LineNumber) || num > int(rg.End.LineNumber) { 1806 continue 1807 } 1808 c0, c1 := 0, len(runes) 1809 if num == int(rg.Start.LineNumber) { 1810 c0 = int(rg.Start.Column) - 1 1811 } 1812 if num == int(rg.End.LineNumber) { 1813 c1 = int(rg.End.Column) - 1 1814 } 1815 c0 = max(0, min(c0, len(runes))) 1816 c1 = max(0, min(c1, len(runes))) 1817 if c0 < c1 { 1818 intervals = append(intervals, [2]int{c0, c1}) 1819 } 1820 } 1821 intervals = mergeIntervals(intervals) 1822 1823 out[i] = ChunkLine{ 1824 Num: num, 1825 Spans: spanRunes(runes, intervals), 1826 Highlight: len(intervals) > 0, 1827 } 1828 } 1829 return out 1830} 1831 1832// FileNameSpans splits a filename into matched/unmatched spans using ranges. 1833// Filename ranges live on line 1; columns are clamped to rune bounds. 1834func FileNameSpans(name string, ranges []zoekt.Range) []ChunkSpan { 1835 runes := []rune(name) 1836 var intervals [][2]int 1837 for _, rg := range ranges { 1838 if rg.Start.LineNumber > 1 || rg.End.LineNumber < 1 { 1839 continue 1840 } 1841 c0 := max(0, min(int(rg.Start.Column)-1, len(runes))) 1842 c1 := max(0, min(int(rg.End.Column)-1, len(runes))) 1843 if c0 < c1 { 1844 intervals = append(intervals, [2]int{c0, c1}) 1845 } 1846 } 1847 return spanRunes(runes, mergeIntervals(intervals)) 1848} 1849 1850type CodeSearchParams struct { 1851 BaseParams 1852 FilterType string // "code" 1853 FilterQuery string 1854 Results []SearchResult 1855 Page pagination.Page 1856 HasMore bool 1857 ErrorMsg string 1858 1859 MatchCount int 1860 FileCount int 1861 TimeTaken time.Duration 1862} 1863 1864func (p *Pages) CodeSearch(w io.Writer, params CodeSearchParams) error { 1865 params.FilterType = "code" 1866 return p.execute("search/search", w, params) 1867} 1868 1869func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1870 return p.execute("timeline/home", w, params) 1871} 1872 1873type CommentBodyFragmentParams struct { 1874 Comment models.Comment 1875 Reactions map[models.ReactionKind]models.ReactionDisplayData 1876 UserReacted map[models.ReactionKind]bool 1877} 1878 1879func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1880 return p.executePlain("fragments/comment/commentBody", w, params) 1881} 1882 1883type PullCommentFragmentParams struct { 1884 LoggedInUser *oauth.MultiAccountUser 1885 Comment models.Comment 1886 Reactions map[models.ReactionKind]models.ReactionDisplayData 1887 UserReacted map[models.ReactionKind]bool 1888 HxSwapOob bool 1889} 1890 1891func (p *Pages) PullCommentFragment(w io.Writer, params PullCommentFragmentParams) error { 1892 return p.executePlain("fragments/comment/pullComment", w, params) 1893} 1894 1895type CommentHeaderFragmentParams struct { 1896 Comment models.Comment 1897 Reactions map[models.ReactionKind]models.ReactionDisplayData 1898 UserReacted map[models.ReactionKind]bool 1899 HxSwapOob bool 1900} 1901 1902func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 1903 return p.executePlain("fragments/comment/commentHeader", w, params) 1904} 1905 1906type EditCommentFragmentParams struct { 1907 Comment models.Comment 1908} 1909 1910func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1911 return p.executePlain("fragments/comment/edit", w, params) 1912} 1913 1914type ReplyCommentFragmentParams struct { 1915 BaseParams 1916} 1917 1918func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1919 return p.executePlain("fragments/comment/reply", w, params) 1920} 1921 1922type ReplyPlaceholderFragmentParams struct { 1923 BaseParams 1924} 1925 1926func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1927 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1928} 1929 1930func (p *Pages) Static() http.Handler { 1931 if p.dev { 1932 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1933 } 1934 1935 sub, err := fs.Sub(p.embedFS, "static") 1936 if err != nil { 1937 p.logger.Error("no static dir found? that's crazy", "err", err) 1938 panic(err) 1939 } 1940 // Custom handler to apply Cache-Control headers for font files 1941 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1942} 1943 1944func Cache(h http.Handler) http.Handler { 1945 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1946 path := strings.Split(r.URL.Path, "?")[0] 1947 1948 if strings.HasSuffix(path, ".css") { 1949 // on day for css files 1950 w.Header().Set("Cache-Control", "public, max-age=86400") 1951 } else { 1952 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1953 } 1954 h.ServeHTTP(w, r) 1955 }) 1956} 1957 1958func (p *Pages) CssContentHash() string { 1959 cssFile, err := p.embedFS.Open("static/tw.css") 1960 if err != nil { 1961 slog.Debug("Error opening CSS file", "err", err) 1962 return "" 1963 } 1964 defer cssFile.Close() 1965 1966 hasher := sha256.New() 1967 if _, err := io.Copy(hasher, cssFile); err != nil { 1968 slog.Debug("Error hashing CSS file", "err", err) 1969 return "" 1970 } 1971 1972 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1973} 1974 1975func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1976 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1977} 1978 1979func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1980 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1981} 1982 1983func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1984 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1985} 1986 1987func (p *Pages) Error500(w io.Writer) error { 1988 return p.execute("errors/500", w, nil) 1989} 1990 1991func (p *Pages) Error404(w io.Writer) error { 1992 return p.execute("errors/404", w, nil) 1993} 1994 1995func (p *Pages) ErrorKnot404(w io.Writer) error { 1996 return p.execute("errors/knot404", w, nil) 1997} 1998 1999func (p *Pages) Error503(w io.Writer) error { 2000 return p.execute("errors/503", w, nil) 2001}