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