Monorepo for Tangled tangled.org
5

Configure Feed

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

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