Monorepo for Tangled tangled.org
5

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 IsStacked bool 1316 Comparison *types.RepoFormatPatchResponse 1317 Diff *types.NiceDiff 1318 DiffOpts types.DiffOpts 1319 StackedDiffs []StackedDiff 1320 MergeCheck *types.MergeCheckResponse 1321 StackTitles map[string]string 1322 StackBodies map[string]string 1323 PrefillError string 1324 Active string 1325 LabelDefs map[string]*models.LabelDefinition 1326 LabelState models.LabelState 1327 StackLabelStates map[string]models.LabelState 1328} 1329 1330func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1331 params.Active = "pulls" 1332 return p.executeRepo("repo/pulls/new", w, params) 1333} 1334 1335func (p *Pages) PullComposeHostFragment(w io.Writer, params RepoNewPullParams) error { 1336 return p.executePlain("repo/pulls/fragments/pullComposeHost", w, params) 1337} 1338 1339func (p *Pages) MarkdownPreviewFragment(w io.Writer, body string) error { 1340 return p.executePlain("fragments/markdownPreview", w, body) 1341} 1342 1343type RepoPullsParams struct { 1344 BaseParams 1345 RepoInfo repoinfo.RepoInfo 1346 Pulls []*models.Pull 1347 Active string 1348 FilterState string 1349 FilterQuery string 1350 BaseFilterQuery string 1351 Stacks []models.Stack 1352 Pipelines map[string]models.Pipeline 1353 LabelDefs map[string]*models.LabelDefinition 1354 Page pagination.Page 1355 PullCount int 1356 VouchRelationships map[syntax.DID]*models.VouchRelationship 1357} 1358 1359func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1360 params.Active = "pulls" 1361 return p.executeRepo("repo/pulls/pulls", w, params) 1362} 1363 1364type ResubmitResult uint64 1365 1366const ( 1367 ShouldResubmit ResubmitResult = iota 1368 ShouldNotResubmit 1369 Unknown 1370) 1371 1372func (r ResubmitResult) Yes() bool { 1373 return r == ShouldResubmit 1374} 1375func (r ResubmitResult) No() bool { 1376 return r == ShouldNotResubmit 1377} 1378func (r ResubmitResult) Unknown() bool { 1379 return r == Unknown 1380} 1381 1382type RepoSinglePullParams struct { 1383 BaseParams 1384 RepoInfo repoinfo.RepoInfo 1385 Active string 1386 Pull *models.Pull 1387 Stack models.Stack 1388 Backlinks []models.RichReferenceLink 1389 BranchDeleteStatus *models.BranchDeleteStatus 1390 MergeCheck types.MergeCheckResponse 1391 ResubmitCheck ResubmitResult 1392 Pipelines map[string]models.Pipeline 1393 Diff types.DiffRenderer 1394 DiffOpts types.DiffOpts 1395 ActiveRound int 1396 IsInterdiff bool 1397 1398 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1399 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1400 1401 LabelDefs map[string]*models.LabelDefinition 1402 VouchRelationships map[syntax.DID]*models.VouchRelationship 1403 VouchSkips map[syntax.DID]bool 1404} 1405 1406func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1407 params.Active = "pulls" 1408 return p.executeRepo("repo/pulls/pull", w, params) 1409} 1410 1411type PullResubmitParams struct { 1412 BaseParams 1413 RepoInfo repoinfo.RepoInfo 1414 Pull *models.Pull 1415 SubmissionId int 1416} 1417 1418func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1419 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1420} 1421 1422type PullActionsParams struct { 1423 BaseParams 1424 RepoInfo repoinfo.RepoInfo 1425 Pull *models.Pull 1426 RoundNumber int 1427 MergeCheck types.MergeCheckResponse 1428 ResubmitCheck ResubmitResult 1429 BranchDeleteStatus *models.BranchDeleteStatus 1430 Stack models.Stack 1431 1432 // renders buttons in a pre-check state and attaches the hx-trigger="load" 1433 // that fetches the real, checked fragment 1434 Loading bool 1435} 1436 1437func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1438 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1439} 1440 1441type PullNewCommentParams struct { 1442 BaseParams 1443 RepoInfo repoinfo.RepoInfo 1444 Pull *models.Pull 1445 RoundNumber int 1446} 1447 1448func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1449 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1450} 1451 1452type RepoCompareParams struct { 1453 BaseParams 1454 RepoInfo repoinfo.RepoInfo 1455 Forks []models.Repo 1456 Branches []types.Branch 1457 Tags []*types.TagReference 1458 Base string 1459 Head string 1460 Diff *types.NiceDiff 1461 DiffOpts types.DiffOpts 1462 1463 Active string 1464} 1465 1466func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1467 params.Active = "overview" 1468 return p.executeRepo("repo/compare/compare", w, params) 1469} 1470 1471type RepoCompareNewParams struct { 1472 BaseParams 1473 RepoInfo repoinfo.RepoInfo 1474 Forks []models.Repo 1475 Branches []types.Branch 1476 Tags []*types.TagReference 1477 Base string 1478 Head string 1479 1480 Active string 1481} 1482 1483func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1484 params.Active = "overview" 1485 return p.executeRepo("repo/compare/new", w, params) 1486} 1487 1488type RepoCompareAllowPullParams struct { 1489 BaseParams 1490 RepoInfo repoinfo.RepoInfo 1491 Base string 1492 Head string 1493} 1494 1495func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1496 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1497} 1498 1499type RepoCompareDiffFragmentParams struct { 1500 Diff types.NiceDiff 1501 DiffOpts types.DiffOpts 1502} 1503 1504func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1505 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1506} 1507 1508type LabelPanelParams struct { 1509 BaseParams 1510 RepoInfo repoinfo.RepoInfo 1511 Defs map[string]*models.LabelDefinition 1512 Subject string 1513 State models.LabelState 1514} 1515 1516func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1517 return p.executePlain("repo/fragments/labelPanel", w, params) 1518} 1519 1520type EditLabelPanelParams struct { 1521 BaseParams 1522 RepoInfo repoinfo.RepoInfo 1523 Defs map[string]*models.LabelDefinition 1524 Subject string 1525 State models.LabelState 1526 Prefix string 1527} 1528 1529func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1530 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1531} 1532 1533type RepoStarsParams struct { 1534 BaseParams 1535 RepoInfo repoinfo.RepoInfo 1536 Active string 1537 Starrers []models.Star 1538 Page pagination.Page 1539 TotalCount int 1540} 1541 1542func (p *Pages) RepoStars(w io.Writer, params RepoStarsParams) error { 1543 params.Active = "overview" 1544 return p.executeRepo("repo/stars", w, params) 1545} 1546 1547type RepoForksParams struct { 1548 BaseParams 1549 RepoInfo repoinfo.RepoInfo 1550 Active string 1551 Forks []models.Repo 1552 Page pagination.Page 1553 TotalCount int 1554} 1555 1556func (p *Pages) RepoForks(w io.Writer, params RepoForksParams) error { 1557 params.Active = "overview" 1558 return p.executeRepo("repo/forks", w, params) 1559} 1560 1561type PipelinesParams struct { 1562 BaseParams 1563 RepoInfo repoinfo.RepoInfo 1564 Pipelines []models.Pipeline 1565 Active string 1566 FilterKind string 1567 Total int64 1568} 1569 1570func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1571 params.Active = "pipelines" 1572 return p.executeRepo("repo/pipelines/pipelines", w, params) 1573} 1574 1575type LogBlockParams struct { 1576 Id int 1577 Name string 1578 Command string 1579 Collapsed bool 1580 StartTime time.Time 1581} 1582 1583func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1584 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1585} 1586 1587type LogBlockEndParams struct { 1588 Id int 1589 StartTime time.Time 1590 EndTime time.Time 1591} 1592 1593func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1594 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1595} 1596 1597type LogLineParams struct { 1598 Id int 1599 Content template.HTML 1600} 1601 1602func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1603 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1604} 1605 1606type WorkflowSymbolOOBParams struct { 1607 Name string 1608 Statuses models.WorkflowStatus 1609} 1610 1611func (p *Pages) WorkflowSymbolOOB(w io.Writer, params WorkflowSymbolOOBParams) error { 1612 return p.executePlain("repo/pipelines/fragments/workflowSymbolOOB", w, params) 1613} 1614 1615type WorkflowParams struct { 1616 BaseParams 1617 RepoInfo repoinfo.RepoInfo 1618 Pipeline models.Pipeline 1619 Workflow string 1620 LogUrl string 1621 Active string 1622} 1623 1624func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1625 params.Active = "pipelines" 1626 return p.executeRepo("repo/pipelines/workflow", w, params) 1627} 1628 1629type PutStringParams struct { 1630 BaseParams 1631 Action string 1632 1633 // this is supplied in the case of editing an existing string 1634 String models.String 1635} 1636 1637func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1638 return p.execute("strings/put", w, params) 1639} 1640 1641type StringsDashboardParams struct { 1642 BaseParams 1643 Card ProfileCard 1644 Strings []models.String 1645} 1646 1647func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1648 return p.execute("strings/dashboard", w, params) 1649} 1650 1651type StringTimelineParams struct { 1652 BaseParams 1653 Strings []models.String 1654} 1655 1656func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1657 return p.execute("strings/timeline", w, params) 1658} 1659 1660type SingleStringParams struct { 1661 BaseParams 1662 ShowRendered bool 1663 RenderToggle bool 1664 RenderedContents template.HTML 1665 String *models.String 1666 Stats models.StringStats 1667 IsStarred bool 1668 StarCount int 1669 Owner identity.Identity 1670 CommentList []models.CommentListItem 1671 1672 Reactions map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData 1673 UserReacted map[syntax.ATURI]map[models.ReactionKind]bool 1674 VouchRelationships map[syntax.DID]*models.VouchRelationship 1675} 1676 1677func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1678 return p.execute("strings/string", w, params) 1679} 1680 1681type SearchReposParams struct { 1682 BaseParams 1683 Repos []models.Repo 1684 Page pagination.Page 1685 ResultCount int 1686 FilterQuery string 1687 SortParam string 1688 TimeTaken time.Duration 1689 DocCount int64 1690} 1691 1692func (p *Pages) SearchRepos(w io.Writer, params SearchReposParams) error { 1693 return p.execute("search/search", w, params) 1694} 1695 1696type SearchQuickParams struct { 1697 Repos []models.Repo 1698 Query string 1699 Total int 1700} 1701 1702func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1703 return p.executePlain("search/fragments/quick", w, params) 1704} 1705 1706func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1707 tpl, err := p.parse("search/fragments/quick") 1708 if err != nil { 1709 return err 1710 } 1711 return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1712} 1713 1714func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1715 return p.execute("timeline/home", w, params) 1716} 1717 1718type CommentBodyFragmentParams struct { 1719 Comment models.Comment 1720 Reactions map[models.ReactionKind]models.ReactionDisplayData 1721 UserReacted map[models.ReactionKind]bool 1722} 1723 1724func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1725 return p.executePlain("fragments/comment/commentBody", w, params) 1726} 1727 1728type CommentHeaderFragmentParams struct { 1729 Comment models.Comment 1730 Reactions map[models.ReactionKind]models.ReactionDisplayData 1731 UserReacted map[models.ReactionKind]bool 1732 HxSwapOob bool 1733} 1734 1735func (p *Pages) CommentHeaderFragment(w io.Writer, params CommentHeaderFragmentParams) error { 1736 return p.executePlain("fragments/comment/commentHeader", w, params) 1737} 1738 1739type EditCommentFragmentParams struct { 1740 Comment models.Comment 1741} 1742 1743func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1744 return p.executePlain("fragments/comment/edit", w, params) 1745} 1746 1747type ReplyCommentFragmentParams struct { 1748 BaseParams 1749} 1750 1751func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1752 return p.executePlain("fragments/comment/reply", w, params) 1753} 1754 1755type ReplyPlaceholderFragmentParams struct { 1756 BaseParams 1757} 1758 1759func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1760 return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1761} 1762 1763func (p *Pages) Static() http.Handler { 1764 if p.dev { 1765 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1766 } 1767 1768 sub, err := fs.Sub(p.embedFS, "static") 1769 if err != nil { 1770 p.logger.Error("no static dir found? that's crazy", "err", err) 1771 panic(err) 1772 } 1773 // Custom handler to apply Cache-Control headers for font files 1774 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1775} 1776 1777func Cache(h http.Handler) http.Handler { 1778 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1779 path := strings.Split(r.URL.Path, "?")[0] 1780 1781 if strings.HasSuffix(path, ".css") { 1782 // on day for css files 1783 w.Header().Set("Cache-Control", "public, max-age=86400") 1784 } else { 1785 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1786 } 1787 h.ServeHTTP(w, r) 1788 }) 1789} 1790 1791func (p *Pages) CssContentHash() string { 1792 cssFile, err := p.embedFS.Open("static/tw.css") 1793 if err != nil { 1794 slog.Debug("Error opening CSS file", "err", err) 1795 return "" 1796 } 1797 defer cssFile.Close() 1798 1799 hasher := sha256.New() 1800 if _, err := io.Copy(hasher, cssFile); err != nil { 1801 slog.Debug("Error hashing CSS file", "err", err) 1802 return "" 1803 } 1804 1805 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1806} 1807 1808func (p *Pages) DangerPasswordTokenStep(w io.Writer) error { 1809 return p.executePlain("user/settings/fragments/dangerPasswordToken", w, nil) 1810} 1811 1812func (p *Pages) DangerPasswordSuccess(w io.Writer) error { 1813 return p.executePlain("user/settings/fragments/dangerPasswordSuccess", w, nil) 1814} 1815 1816func (p *Pages) DangerDeleteTokenStep(w io.Writer) error { 1817 return p.executePlain("user/settings/fragments/dangerDeleteToken", w, nil) 1818} 1819 1820func (p *Pages) Error500(w io.Writer) error { 1821 return p.execute("errors/500", w, nil) 1822} 1823 1824func (p *Pages) Error404(w io.Writer) error { 1825 return p.execute("errors/404", w, nil) 1826} 1827 1828func (p *Pages) ErrorKnot404(w io.Writer) error { 1829 return p.execute("errors/knot404", w, nil) 1830} 1831 1832func (p *Pages) Error503(w io.Writer) error { 1833 return p.execute("errors/503", w, nil) 1834}