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