Monorepo for Tangled tangled.org
6

Configure Feed

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

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