Monorepo for Tangled tangled.org
9

Configure Feed

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

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