Monorepo for Tangled tangled.org
2

Configure Feed

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

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