Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

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