Monorepo for Tangled tangled.org

appview/pages/markup: smart commit autolink renderer #1003

merged opened by boltless.me targeting master from sl/uupvwnkzxzom
Labels

None yet.

assignee

None yet.

Participants 3
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mcufej76qu22
+336 -216
Diff #4
+11
appview/config/config.go
··· 25 TmpAltAppPassword string `env:"ALT_APP_PASSWORD"` 26 } 27 28 type OAuthConfig struct { 29 ClientSecret string `env:"CLIENT_SECRET"` 30 ClientKid string `env:"CLIENT_KID"`
··· 25 TmpAltAppPassword string `env:"ALT_APP_PASSWORD"` 26 } 27 28 + func (c *CoreConfig) UseTLS() bool { 29 + return !c.Dev 30 + } 31 + 32 + func (c *CoreConfig) BaseUrl() string { 33 + if c.UseTLS() { 34 + return "https://" + c.AppviewHost 35 + } 36 + return "http://" + c.AppviewHost 37 + } 38 + 39 type OAuthConfig struct { 40 ClientSecret string `env:"CLIENT_SECRET"` 41 ClientKid string `env:"CLIENT_KID"`
+1 -4
appview/models/repo.go
··· 130 131 // current display mode 132 ShowingRendered bool // currently in rendered mode 133 134 // content type flags 135 ContentType BlobContentType ··· 150 // no view available, only raw 151 return !(b.HasRenderedView || b.HasTextView) 152 } 153 - 154 - func (b BlobView) ShowingText() bool { 155 - return !b.ShowingRendered 156 - }
··· 130 131 // current display mode 132 ShowingRendered bool // currently in rendered mode 133 + ShowingText bool // currently in text/code mode 134 135 // content type flags 136 ContentType BlobContentType ··· 151 // no view available, only raw 152 return !(b.HasRenderedView || b.HasTextView) 153 }
-18
appview/pages/funcmap.go
··· 332 } 333 return dict, nil 334 }, 335 - "queryParams": func(params ...any) (url.Values, error) { 336 - if len(params)%2 != 0 { 337 - return nil, errors.New("invalid queryParams call") 338 - } 339 - vals := make(url.Values, len(params)/2) 340 - for i := 0; i < len(params); i += 2 { 341 - key, ok := params[i].(string) 342 - if !ok { 343 - return nil, errors.New("queryParams keys must be strings") 344 - } 345 - v, ok := params[i+1].(string) 346 - if !ok { 347 - return nil, errors.New("queryParams values must be strings") 348 - } 349 - vals.Add(key, v) 350 - } 351 - return vals, nil 352 - }, 353 "deref": func(v any) any { 354 val := reflect.ValueOf(v) 355 if val.Kind() == reflect.Pointer && !val.IsNil() {
··· 332 } 333 return dict, nil 334 }, 335 "deref": func(v any) any { 336 val := reflect.ValueOf(v) 337 if val.Kind() == reflect.Pointer && !val.IsNil() {
+149
appview/pages/markup/extension/tangledlink.go
···
··· 1 + package extension 2 + 3 + import ( 4 + "net/url" 5 + "strings" 6 + 7 + "github.com/yuin/goldmark" 8 + "github.com/yuin/goldmark/ast" 9 + "github.com/yuin/goldmark/parser" 10 + "github.com/yuin/goldmark/renderer" 11 + "github.com/yuin/goldmark/text" 12 + "github.com/yuin/goldmark/util" 13 + ) 14 + 15 + // KindTangledLink is a NodeKind of the TangledLink node. 16 + var KindTangledLink = ast.NewNodeKind("TangledLink") 17 + 18 + type TangledLinkNode struct { 19 + ast.BaseInline 20 + Destination string 21 + Commit *TangledCommitLink 22 + // TODO: add more Tangled-link types 23 + } 24 + 25 + type TangledCommitLink struct { 26 + Sha string 27 + } 28 + 29 + var _ ast.Node = new(TangledLinkNode) 30 + 31 + // Dump implements [ast.Node]. 32 + func (n *TangledLinkNode) Dump(source []byte, level int) { 33 + ast.DumpHelper(n, source, level, nil, nil) 34 + } 35 + 36 + // Kind implements [ast.Node]. 37 + func (n *TangledLinkNode) Kind() ast.NodeKind { 38 + return KindTangledLink 39 + } 40 + 41 + type tangledLinkTransformer struct { 42 + host string 43 + } 44 + 45 + var _ parser.ASTTransformer = new(tangledLinkTransformer) 46 + 47 + // Transform implements [parser.ASTTransformer]. 48 + func (t *tangledLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 49 + ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 50 + if !entering { 51 + return ast.WalkContinue, nil 52 + } 53 + 54 + var dest string 55 + 56 + switch n := n.(type) { 57 + case *ast.AutoLink: 58 + dest = string(n.URL(reader.Source())) 59 + case *ast.Link: 60 + // maybe..? not sure 61 + default: 62 + return ast.WalkContinue, nil 63 + } 64 + 65 + if sha := t.parseLinkCommitSha(dest); sha != "" { 66 + newLink := &TangledLinkNode{ 67 + Destination: dest, 68 + Commit: &TangledCommitLink{ 69 + Sha: sha, 70 + }, 71 + } 72 + n.Parent().ReplaceChild(n.Parent(), n, newLink) 73 + } 74 + 75 + return ast.WalkContinue, nil 76 + }) 77 + } 78 + 79 + func (t *tangledLinkTransformer) parseLinkCommitSha(raw string) string { 80 + u, err := url.Parse(raw) 81 + if err != nil || u.Host != t.host { 82 + return "" 83 + } 84 + 85 + // /{owner}/{repo}/commit/<sha> 86 + parts := strings.Split(strings.Trim(u.Path, "/"), "/") 87 + if len(parts) != 4 || parts[2] != "commit" { 88 + return "" 89 + } 90 + 91 + sha := parts[3] 92 + 93 + // basic sha validation 94 + if len(sha) < 7 { 95 + return "" 96 + } 97 + for _, c := range sha { 98 + if !strings.ContainsRune("0123456789abcdef", c) { 99 + return "" 100 + } 101 + } 102 + 103 + return sha[:8] 104 + } 105 + 106 + type tangledLinkRenderer struct{} 107 + 108 + var _ renderer.NodeRenderer = new(tangledLinkRenderer) 109 + 110 + // RegisterFuncs implements [renderer.NodeRenderer]. 111 + func (r *tangledLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 112 + reg.Register(KindTangledLink, r.renderTangledLink) 113 + } 114 + 115 + func (r *tangledLinkRenderer) renderTangledLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 116 + link := node.(*TangledLinkNode) 117 + 118 + if link.Commit != nil { 119 + if entering { 120 + w.WriteString(`<a href="`) 121 + w.WriteString(link.Destination) 122 + w.WriteString(`"><code>`) 123 + w.WriteString(link.Commit.Sha) 124 + } else { 125 + w.WriteString(`</code></a>`) 126 + } 127 + } 128 + 129 + return ast.WalkContinue, nil 130 + } 131 + 132 + type tangledLinkExt struct { 133 + host string 134 + } 135 + 136 + var _ goldmark.Extender = new(tangledLinkExt) 137 + 138 + func (e *tangledLinkExt) Extend(m goldmark.Markdown) { 139 + m.Parser().AddOptions(parser.WithASTTransformers( 140 + util.Prioritized(&tangledLinkTransformer{host: e.host}, 500), 141 + )) 142 + m.Renderer().AddOptions(renderer.WithNodeRenderers( 143 + util.Prioritized(&tangledLinkRenderer{}, 500), 144 + )) 145 + } 146 + 147 + func NewTangledLinkExt(host string) goldmark.Extender { 148 + return &tangledLinkExt{host} 149 + }
+4 -2
appview/pages/markup/markdown.go
··· 46 CamoSecret string 47 repoinfo.RepoInfo 48 IsDev bool 49 RendererType RendererType 50 Sanitizer Sanitizer 51 Files fs.FS 52 } 53 54 - func NewMarkdown() goldmark.Markdown { 55 md := goldmark.New( 56 goldmark.WithExtensions( 57 extension.GFM, ··· 67 ), 68 callout.CalloutExtention, 69 textension.AtExt, 70 emoji.Emoji, 71 ), 72 goldmark.WithParserOptions( ··· 78 } 79 80 func (rctx *RenderContext) RenderMarkdown(source string) string { 81 - return rctx.RenderMarkdownWith(source, NewMarkdown()) 82 } 83 84 func (rctx *RenderContext) RenderMarkdownWith(source string, md goldmark.Markdown) string {
··· 46 CamoSecret string 47 repoinfo.RepoInfo 48 IsDev bool 49 + Hostname string 50 RendererType RendererType 51 Sanitizer Sanitizer 52 Files fs.FS 53 } 54 55 + func NewMarkdown(hostname string) goldmark.Markdown { 56 md := goldmark.New( 57 goldmark.WithExtensions( 58 extension.GFM, ··· 68 ), 69 callout.CalloutExtention, 70 textension.AtExt, 71 + textension.NewTangledLinkExt(hostname), 72 emoji.Emoji, 73 ), 74 goldmark.WithParserOptions( ··· 80 } 81 82 func (rctx *RenderContext) RenderMarkdown(source string) string { 83 + return rctx.RenderMarkdownWith(source, NewMarkdown(rctx.Hostname)) 84 } 85 86 func (rctx *RenderContext) RenderMarkdownWith(source string, md goldmark.Markdown) string {
+2 -2
appview/pages/markup/markdown_test.go
··· 50 51 for _, tt := range tests { 52 t.Run(tt.name, func(t *testing.T) { 53 - md := NewMarkdown() 54 55 var buf bytes.Buffer 56 if err := md.Convert([]byte(tt.markdown), &buf); err != nil { ··· 105 106 for _, tt := range tests { 107 t.Run(tt.name, func(t *testing.T) { 108 - md := NewMarkdown() 109 110 var buf bytes.Buffer 111 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
··· 50 51 for _, tt := range tests { 52 t.Run(tt.name, func(t *testing.T) { 53 + md := NewMarkdown("tangled.org") 54 55 var buf bytes.Buffer 56 if err := md.Convert([]byte(tt.markdown), &buf); err != nil { ··· 105 106 for _, tt := range tests { 107 t.Run(tt.name, func(t *testing.T) { 108 + md := NewMarkdown("tangled.org") 109 110 var buf bytes.Buffer 111 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
+4 -7
appview/pages/markup/reference_link.go
··· 18 // like issues, PRs, comments or even @-mentions 19 // This funciton doesn't actually check for the existence of records in the DB 20 // or the PDS; it merely returns a list of what are presumed to be references. 21 - func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) { 22 var ( 23 refLinkSet = make(map[models.ReferenceLink]struct{}) 24 mentionsSet = make(map[string]struct{}) 25 - md = NewMarkdown() 26 sourceBytes = []byte(source) 27 root = md.Parser().Parse(text.NewReader(sourceBytes)) 28 ) 29 - // trim url scheme. the SSL shouldn't matter 30 - baseUrl = strings.TrimPrefix(baseUrl, "https://") 31 - baseUrl = strings.TrimPrefix(baseUrl, "http://") 32 33 ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 34 if !entering { ··· 41 return ast.WalkSkipChildren, nil 42 case ast.KindLink: 43 dest := string(n.(*ast.Link).Destination) 44 - ref := parseTangledLink(baseUrl, dest) 45 if ref != nil { 46 refLinkSet[*ref] = struct{}{} 47 } ··· 50 an := n.(*ast.AutoLink) 51 if an.AutoLinkType == ast.AutoLinkURL { 52 dest := string(an.URL(sourceBytes)) 53 - ref := parseTangledLink(baseUrl, dest) 54 if ref != nil { 55 refLinkSet[*ref] = struct{}{} 56 }
··· 18 // like issues, PRs, comments or even @-mentions 19 // This funciton doesn't actually check for the existence of records in the DB 20 // or the PDS; it merely returns a list of what are presumed to be references. 21 + func FindReferences(host string, source string) ([]string, []models.ReferenceLink) { 22 var ( 23 refLinkSet = make(map[models.ReferenceLink]struct{}) 24 mentionsSet = make(map[string]struct{}) 25 + md = NewMarkdown(host) 26 sourceBytes = []byte(source) 27 root = md.Parser().Parse(text.NewReader(sourceBytes)) 28 ) 29 30 ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 31 if !entering { ··· 38 return ast.WalkSkipChildren, nil 39 case ast.KindLink: 40 dest := string(n.(*ast.Link).Destination) 41 + ref := parseTangledLink(host, dest) 42 if ref != nil { 43 refLinkSet[*ref] = struct{}{} 44 } ··· 47 an := n.(*ast.AutoLink) 48 if an.AutoLinkType == ast.AutoLinkURL { 49 dest := string(an.URL(sourceBytes)) 50 + ref := parseTangledLink(host, dest) 51 if ref != nil { 52 refLinkSet[*ref] = struct{}{} 53 }
+1
appview/pages/pages.go
··· 53 // initialized with safe defaults, can be overriden per use 54 rctx := &markup.RenderContext{ 55 IsDev: config.Core.Dev, 56 CamoUrl: config.Camo.Host, 57 CamoSecret: config.Camo.SharedSecret, 58 Sanitizer: markup.NewSanitizer(),
··· 53 // initialized with safe defaults, can be overriden per use 54 rctx := &markup.RenderContext{ 55 IsDev: config.Core.Dev, 56 + Hostname: config.Core.AppviewHost, 57 CamoUrl: config.Camo.Host, 58 CamoSecret: config.Camo.SharedSecret, 59 Sanitizer: markup.NewSanitizer(),
+2 -2
appview/pages/templates/fragments/pagination.html
··· 1 {{ define "fragments/pagination" }} 2 - {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (url.Values) */}} 3 {{ $page := .Page }} 4 {{ $totalCount := .TotalCount }} 5 {{ $basePath := .BasePath }} 6 - {{ $queryParams := safeUrl .QueryParams.Encode }} 7 8 {{ $prev := $page.Previous.Offset }} 9 {{ $next := $page.Next.Offset }}
··· 1 {{ define "fragments/pagination" }} 2 + {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (string) */}} 3 {{ $page := .Page }} 4 {{ $totalCount := .TotalCount }} 5 {{ $basePath := .BasePath }} 6 + {{ $queryParams := .QueryParams }} 7 8 {{ $prev := $page.Previous.Offset }} 9 {{ $next := $page.Next.Offset }}
+1 -1
appview/pages/templates/repo/blob.html
··· 35 36 {{ if .BlobView.ShowingText }} 37 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 38 - <span>{{ .BlobView.Lines }} lines</span> 39 {{ end }} 40 41 {{ if .BlobView.SizeHint }}
··· 35 36 {{ if .BlobView.ShowingText }} 37 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 38 + <span>{{ .Lines }} lines</span> 39 {{ end }} 40 41 {{ if .BlobView.SizeHint }}
+3 -3
appview/pages/templates/repo/fragments/splitDiff.html
··· 1 {{ define "repo/fragments/splitDiff" }} 2 {{ $name := .Id }} 3 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 4 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/50 target:dark:bg-yellow-700/50 scroll-mt-48 group/line" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
··· 1 {{ define "repo/fragments/splitDiff" }} 2 {{ $name := .Id }} 3 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
+3 -3
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 3 <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 7 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/30 target:dark:bg-yellow-700/30 scroll-mt-48 group/line" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
··· 3 <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 {{- $oldStart := .OldPosition -}} 5 {{- $newStart := .NewPosition -}} 6 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 {{- $lineNrSepStyle1 := "" -}} 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200 target:dark:bg-yellow-700 scroll-mt-48" -}} 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
+1 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ $handle := resolve .Comment.Did }} 4 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 {{ template "hats" $ }} 6 <span class="before:content-['·']"></span> 7 {{ template "timestamp" . }}
··· 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 + {{ resolve .Comment.Did }} 4 {{ template "hats" $ }} 5 <span class="before:content-['·']"></span> 6 {{ template "timestamp" . }}
+1 -1
appview/pages/templates/repo/issues/fragments/newComment.html
··· 12 <textarea 13 id="comment-textarea" 14 name="body" 15 - class="w-full p-2 rounded" 16 placeholder="Add to the discussion. Markdown is supported." 17 onkeyup="updateCommentForm()" 18 rows="5"
··· 12 <textarea 13 id="comment-textarea" 14 name="body" 15 + class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 16 placeholder="Add to the discussion. Markdown is supported." 17 onkeyup="updateCommentForm()" 18 rows="5"
+1 -1
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
··· 8 /> 9 {{ end }} 10 <input 11 - class="w-full p-0 border-none focus:outline-none bg-transparent" 12 placeholder="Leave a reply..." 13 hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 14 hx-trigger="focus"
··· 8 /> 9 {{ end }} 10 <input 11 + class="w-full p-0 border-none focus:outline-none" 12 placeholder="Leave a reply..." 13 hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 14 hx-trigger="focus"
+1 -1
appview/pages/templates/repo/issues/issues.html
··· 80 "Page" .Page 81 "TotalCount" .IssueCount 82 "BasePath" (printf "/%s/issues" .RepoInfo.FullName) 83 - "QueryParams" (queryParams "state" $state "q" .FilterQuery) 84 ) }} 85 {{ end }} 86 {{ end }}
··· 80 "Page" .Page 81 "TotalCount" .IssueCount 82 "BasePath" (printf "/%s/issues" .RepoInfo.FullName) 83 + "QueryParams" (printf "state=%s&q=%s" $state .FilterQuery) 84 ) }} 85 {{ end }} 86 {{ end }}
+1 -1
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 12 > 13 <textarea 14 name="body" 15 - class="w-full p-2 rounded border" 16 rows=8 17 placeholder="Add to the discussion..."></textarea 18 >
··· 12 > 13 <textarea 14 name="body" 15 + class="w-full p-2 rounded border border-gray-200" 16 rows=8 17 placeholder="Add to the discussion..."></textarea 18 >
+58 -111
appview/pages/templates/repo/pulls/pull.html
··· 22 <script> 23 (function() { 24 const details = document.getElementById('bottomSheet'); 25 - const backdrop = document.getElementById('bottomSheetBackdrop'); 26 const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 27 28 - // function to update backdrop 29 - const updateBackdrop = () => { 30 - if (backdrop) { 31 - if (details.open && !isDesktop()) { 32 - backdrop.classList.remove('opacity-0', 'pointer-events-none'); 33 - backdrop.classList.add('opacity-100', 'pointer-events-auto'); 34 - document.body.style.overflow = 'hidden'; 35 - } else { 36 - backdrop.classList.remove('opacity-100', 'pointer-events-auto'); 37 - backdrop.classList.add('opacity-0', 'pointer-events-none'); 38 - document.body.style.overflow = ''; 39 - } 40 - } 41 - }; 42 - 43 // close on mobile initially 44 if (!isDesktop()) { 45 details.open = false; 46 } 47 - updateBackdrop(); // initialize backdrop 48 49 // prevent closing on desktop 50 details.addEventListener('toggle', function(e) { 51 if (isDesktop() && !this.open) { 52 this.open = true; 53 } 54 - updateBackdrop(); 55 }); 56 57 const mediaQuery = window.matchMedia('(min-width: 768px)'); ··· 63 // switched to mobile - close 64 details.open = false; 65 } 66 - updateBackdrop(); 67 }); 68 - 69 - // close when clicking backdrop 70 - if (backdrop) { 71 - backdrop.addEventListener('click', () => { 72 - if (!isDesktop()) { 73 - details.open = false; 74 - } 75 - }); 76 - } 77 })(); 78 </script> 79 {{ end }} ··· 137 {{ define "subsPanel" }} 138 {{ $root := index . 2 }} 139 {{ $pull := $root.Pull }} 140 <!-- backdrop overlay - only visible on mobile when open --> 141 - <div id="bottomSheetBackdrop" class="fixed inset-0 bg-black/50 md:hidden opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div> 142 <!-- right panel - bottom sheet on mobile, side panel on desktop --> 143 <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 144 - <details open id="bottomSheet" class="rounded-t-2xl md:rounded-t drop-shadow-lg md:drop-shadow-none group/panel"> 145 <summary class=" 146 flex gap-4 items-center justify-between 147 rounded-t-2xl md:rounded-t cursor-pointer list-none p-4 md:h-12 ··· 150 md:bg-white md:dark:bg-gray-800 151 drop-shadow-sm 152 border-t md:border-x md:border-t-0 border-gray-200 dark:border-gray-700"> 153 - <h2 class="">History</h2> 154 {{ template "subsPanelSummary" $ }} 155 </summary> 156 <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> ··· 163 {{ define "subsPanelSummary" }} 164 {{ $root := index . 2 }} 165 {{ $pull := $root.Pull }} 166 - {{ $rounds := len $pull.Submissions }} 167 - {{ $comments := $pull.TotalComments }} 168 <div class="flex items-center gap-2 text-sm"> 169 - <span> 170 - {{ $rounds }} round{{ if ne $rounds 1 }}s{{ end }} 171 - </span> 172 - <span class="select-none before:content-['\00B7']"></span> 173 - <span> 174 - {{ $comments }} comment{{ if ne $comments 1 }}s{{ end }} 175 - </span> 176 - 177 <span class="md:hidden inline"> 178 <span class="inline group-open:hidden">{{ i "chevron-up" "size-4" }}</span> 179 <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> ··· 233 {{ $idx := index . 1 }} 234 {{ $lastIdx := index . 2 }} 235 {{ $root := index . 3 }} 236 - {{ $round := $item.RoundNumber }} 237 - <div class=" 238 - w-full shadow-sm bg-gray-50 dark:bg-gray-900 border-2 border-t-0 239 - {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 240 - {{ if eq $round $root.ActiveRound }} 241 - border-blue-200 dark:border-blue-700 242 - {{ else }} 243 - border-gray-200 dark:border-gray-700 244 - {{ end }} 245 - "> 246 {{ template "submissionHeader" $ }} 247 {{ template "submissionComments" $ }} 248 </div> 249 {{ end }} 250 ··· 256 <div class=" 257 {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 258 px-6 py-4 pr-2 pt-2 259 - bg-white dark:bg-gray-800 260 - {{ if eq $round $root.ActiveRound }} 261 - border-t-2 border-blue-200 dark:border-blue-700 262 {{ else }} 263 - border-b-2 border-gray-200 dark:border-gray-700 264 {{ end }} 265 flex gap-2 sticky top-0 z-20"> 266 <!-- left column: just profile picture --> ··· 290 {{ $round := $item.RoundNumber }} 291 <div class="flex gap-2 items-center justify-between mb-1"> 292 <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 293 - {{ $handle := resolve $root.Pull.OwnerDid }} 294 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 295 - submitted 296 - <span class="px-2 py-0.5 text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded font-mono text-xs border"> 297 #{{ $round }} 298 </span> 299 <span class="select-none before:content-['\00B7']"></span> ··· 515 516 {{ define "submissionComments" }} 517 {{ $item := index . 0 }} 518 - {{ $idx := index . 1 }} 519 - {{ $lastIdx := index . 2 }} 520 - {{ $root := index . 3 }} 521 - {{ $round := $item.RoundNumber }} 522 - {{ $c := len $item.Comments }} 523 - <details class="relative ml-10 group/comments" {{ if or (eq $c 0) (eq $root.ActiveRound $round) }}open{{ end }}> 524 - <summary class="cursor-pointer list-none"> 525 - <div class="hidden group-open/comments:block absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center group/border z-4"> 526 - <div class="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-0.5 group-open/comments:bg-gray-200 dark:group-open/comments:bg-gray-700 group-hover/border:bg-gray-400 dark:group-hover/border:bg-gray-500 transition-colors"> </div> 527 - </div> 528 - <div class="group-open/comments:hidden block relative group/summary py-4"> 529 - <div class="absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center z-4"> 530 - <div class="absolute left-1/2 -translate-x-1/2 h-1/3 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-gray-700 group-hover/summary:bg-gray-400 dark:group-hover/summary:bg-gray-500 transition-colors"></div> 531 - </div> 532 - <span class="text-gray-500 dark:text-gray-400 text-sm group-hover/summary:text-gray-600 dark:group-hover/summary:text-gray-300 transition-colors flex items-center gap-2 -ml-2 relative"> 533 - {{ i "circle-plus" "size-4 z-5" }} 534 - expand {{ $c }} comment{{ if ne $c 1 }}s{{ end }} 535 - </span> 536 - </div> 537 - </summary> 538 - <div> 539 - {{ range $item.Comments }} 540 - {{ template "submissionComment" . }} 541 - {{ end }} 542 - </div> 543 - 544 - <div class="relative -ml-10"> 545 - {{ if eq $lastIdx $item.RoundNumber }} 546 - {{ block "mergeStatus" $root }} {{ end }} 547 - {{ block "resubmitStatus" $root }} {{ end }} 548 - {{ end }} 549 - </div> 550 - <div class="relative -ml-10 bg-gray-50 dark:bg-gray-900"> 551 - {{ if $root.LoggedInUser }} 552 - {{ template "repo/pulls/fragments/pullActions" 553 - (dict 554 - "LoggedInUser" $root.LoggedInUser 555 - "Pull" $root.Pull 556 - "RepoInfo" $root.RepoInfo 557 - "RoundNumber" $item.RoundNumber 558 - "MergeCheck" $root.MergeCheck 559 - "ResubmitCheck" $root.ResubmitCheck 560 - "BranchDeleteStatus" $root.BranchDeleteStatus 561 - "Stack" $root.Stack) }} 562 - {{ end }} 563 - </div> 564 - </details> 565 {{ end }} 566 567 {{ define "submissionComment" }} 568 <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 569 <!-- left column: profile picture --> 570 - <div class="flex-shrink-0 h-fit relative"> 571 <img 572 src="{{ tinyAvatar .OwnerDid }}" 573 alt="" 574 - class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-5" 575 /> 576 </div> 577 <!-- right column: name and body in two rows --> 578 <div class="flex-1 min-w-0"> 579 <!-- Row 1: Author and timestamp --> 580 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 581 - {{ $handle := resolve .OwnerDid }} 582 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 583 <span class="before:content-['·']"></span> 584 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 585 - {{ template "repo/fragments/shortTime" .Created }} 586 </a> 587 </div> 588 <!-- Row 2: Body text -->
··· 22 <script> 23 (function() { 24 const details = document.getElementById('bottomSheet'); 25 const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 26 27 // close on mobile initially 28 if (!isDesktop()) { 29 details.open = false; 30 } 31 32 // prevent closing on desktop 33 details.addEventListener('toggle', function(e) { 34 if (isDesktop() && !this.open) { 35 this.open = true; 36 } 37 }); 38 39 const mediaQuery = window.matchMedia('(min-width: 768px)'); ··· 45 // switched to mobile - close 46 details.open = false; 47 } 48 }); 49 })(); 50 </script> 51 {{ end }} ··· 109 {{ define "subsPanel" }} 110 {{ $root := index . 2 }} 111 {{ $pull := $root.Pull }} 112 + 113 <!-- backdrop overlay - only visible on mobile when open --> 114 + <div class=" 115 + fixed inset-0 bg-black/50 z-50 md:hidden opacity-0 116 + pointer-events-none transition-opacity duration-300 117 + has-[~#subs_details[open]]:opacity-100 has-[~#subs_details[open]]:pointer-events-auto"> 118 + </div> 119 <!-- right panel - bottom sheet on mobile, side panel on desktop --> 120 <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 121 + <details open id="bottomSheet" class="group rounded-t-2xl md:rounded-t drop-shadow-lg md:drop-shadow-none"> 122 <summary class=" 123 flex gap-4 items-center justify-between 124 rounded-t-2xl md:rounded-t cursor-pointer list-none p-4 md:h-12 ··· 127 md:bg-white md:dark:bg-gray-800 128 drop-shadow-sm 129 border-t md:border-x md:border-t-0 border-gray-200 dark:border-gray-700"> 130 + <h2 class="">Submissions</h2> 131 {{ template "subsPanelSummary" $ }} 132 </summary> 133 <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> ··· 140 {{ define "subsPanelSummary" }} 141 {{ $root := index . 2 }} 142 {{ $pull := $root.Pull }} 143 + {{ $latest := $pull.LastRoundNumber }} 144 <div class="flex items-center gap-2 text-sm"> 145 + <!--{{ if $root.IsInterdiff }} 146 + <span> 147 + viewing interdiff of 148 + <span class="font-mono">#{{ $root.ActiveRound }}</span> 149 + and 150 + <span class="font-mono">#{{ sub $root.ActiveRound 1 }}</span> 151 + </span> 152 + {{ else }} 153 + {{ if ne $root.ActiveRound $latest }} 154 + <span>(outdated)</span> 155 + <span class="before:content-['·']"></span> 156 + <a class="underline" href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $latest }}?{{ safeUrl $root.DiffOpts.Encode }}"> 157 + view latest 158 + </a> 159 + {{ end }} 160 + {{ end }}--> 161 <span class="md:hidden inline"> 162 <span class="inline group-open:hidden">{{ i "chevron-up" "size-4" }}</span> 163 <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> ··· 217 {{ $idx := index . 1 }} 218 {{ $lastIdx := index . 2 }} 219 {{ $root := index . 3 }} 220 + <div class="{{ if eq $item.RoundNumber 0 }}rounded-b border-t-0{{ else }}rounded{{ end }} border border-gray-200 dark:border-gray-700 w-full shadow-sm bg-gray-50 dark:bg-gray-800/50"> 221 {{ template "submissionHeader" $ }} 222 {{ template "submissionComments" $ }} 223 + 224 + {{ if eq $lastIdx $item.RoundNumber }} 225 + {{ block "mergeStatus" $root }} {{ end }} 226 + {{ block "resubmitStatus" $root }} {{ end }} 227 + {{ end }} 228 + 229 + {{ if $root.LoggedInUser }} 230 + {{ template "repo/pulls/fragments/pullActions" 231 + (dict 232 + "LoggedInUser" $root.LoggedInUser 233 + "Pull" $root.Pull 234 + "RepoInfo" $root.RepoInfo 235 + "RoundNumber" $item.RoundNumber 236 + "MergeCheck" $root.MergeCheck 237 + "ResubmitCheck" $root.ResubmitCheck 238 + "BranchDeleteStatus" $root.BranchDeleteStatus 239 + "Stack" $root.Stack) }} 240 + {{ end }} 241 </div> 242 {{ end }} 243 ··· 249 <div class=" 250 {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 251 px-6 py-4 pr-2 pt-2 252 + {{ if eq $root.ActiveRound $round }} 253 + bg-blue-100 dark:bg-blue-900 border-b border-blue-200 dark:border-blue-700 254 {{ else }} 255 + bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 256 {{ end }} 257 flex gap-2 sticky top-0 z-20"> 258 <!-- left column: just profile picture --> ··· 282 {{ $round := $item.RoundNumber }} 283 <div class="flex gap-2 items-center justify-between mb-1"> 284 <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 285 + {{ resolve $root.Pull.OwnerDid }} submitted 286 + <span class="px-2 py-0.5 {{ if eq $root.ActiveRound $round }}text-white bg-blue-600 dark:bg-blue-500 border-blue-700 dark:border-blue-600{{ else }}text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600{{ end }} rounded font-mono text-xs border"> 287 #{{ $round }} 288 </span> 289 <span class="select-none before:content-['\00B7']"></span> ··· 505 506 {{ define "submissionComments" }} 507 {{ $item := index . 0 }} 508 + <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 509 + {{ range $item.Comments }} 510 + {{ template "submissionComment" . }} 511 + {{ end }} 512 + </div> 513 {{ end }} 514 515 {{ define "submissionComment" }} 516 <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 517 <!-- left column: profile picture --> 518 + <div class="flex-shrink-0"> 519 <img 520 src="{{ tinyAvatar .OwnerDid }}" 521 alt="" 522 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 523 /> 524 </div> 525 <!-- right column: name and body in two rows --> 526 <div class="flex-1 min-w-0"> 527 <!-- Row 1: Author and timestamp --> 528 <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 529 + <span>{{ resolve .OwnerDid }}</span> 530 <span class="before:content-['·']"></span> 531 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 532 + {{ template "repo/fragments/time" .Created }} 533 </a> 534 </div> 535 <!-- Row 2: Body text -->
+1 -1
appview/pages/templates/repo/pulls/pulls.html
··· 166 "Page" .Page 167 "TotalCount" .PullCount 168 "BasePath" (printf "/%s/pulls" .RepoInfo.FullName) 169 - "QueryParams" (queryParams "state" .FilteringBy.String "q" .FilterQuery) 170 ) }} 171 {{ end }} 172 {{ end }}
··· 166 "Page" .Page 167 "TotalCount" .PullCount 168 "BasePath" (printf "/%s/pulls" .RepoInfo.FullName) 169 + "QueryParams" (printf "state=%s&q=%s" .FilteringBy.String .FilterQuery) 170 ) }} 171 {{ end }} 172 {{ end }}
+1 -1
appview/pulls/opengraph.go
··· 199 currentX += commentTextWidth + 40 200 201 // Draw files changed 202 - err = statusStatsArea.DrawLucideIcon("file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 203 if err != nil { 204 log.Printf("failed to draw file diff icon: %v", err) 205 }
··· 199 currentX += commentTextWidth + 40 200 201 // Draw files changed 202 + err = statusStatsArea.DrawLucideIcon("static/icons/file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 203 if err != nil { 204 log.Printf("failed to draw file diff icon: %v", err) 205 }
+1 -1
appview/pulls/pulls.go
··· 228 reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 229 if err != nil { 230 log.Println("failed to get pull reactions") 231 } 232 233 userReactions := map[models.ReactionKind]bool{} ··· 1874 record := pull.AsRecord() 1875 record.PatchBlob = blob.Blob 1876 record.CreatedAt = time.Now().Format(time.RFC3339) 1877 - record.Source.Sha = newSourceRev 1878 1879 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1880 Collection: tangled.RepoPullNSID,
··· 228 reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 229 if err != nil { 230 log.Println("failed to get pull reactions") 231 + s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.") 232 } 233 234 userReactions := map[models.ReactionKind]bool{} ··· 1875 record := pull.AsRecord() 1876 record.PatchBlob = blob.Blob 1877 record.CreatedAt = time.Now().Format(time.RFC3339) 1878 1879 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1880 Collection: tangled.RepoPullNSID,
+1 -1
appview/repo/archive.go
··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 - rp.config.Core.AppviewHost, f.DidSlashRepo(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 + rp.config.Core.BaseUrl(), f.DidSlashRepo(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
+2 -17
appview/repo/blob.go
··· 219 if resp.Content != nil { 220 bytes, _ := base64.StdEncoding.DecodeString(*resp.Content) 221 view.Contents = string(bytes) 222 - view.Lines = countLines(view.Contents) 223 } 224 225 case ".mp4", ".webm", ".ogg", ".mov", ".avi": ··· 238 239 if resp.Content != nil { 240 view.Contents = *resp.Content 241 - view.Lines = countLines(view.Contents) 242 } 243 244 // with text, we may be dealing with markdown ··· 291 } 292 return slices.Contains(textualTypes, mimeType) 293 } 294 - 295 - // TODO: dedup with strings 296 - func countLines(content string) int { 297 - if content == "" { 298 - return 0 299 - } 300 - 301 - count := strings.Count(content, "\n") 302 - 303 - if !strings.HasSuffix(content, "\n") { 304 - count++ 305 - } 306 - 307 - return count 308 - }
··· 219 if resp.Content != nil { 220 bytes, _ := base64.StdEncoding.DecodeString(*resp.Content) 221 view.Contents = string(bytes) 222 + view.Lines = strings.Count(view.Contents, "\n") + 1 223 } 224 225 case ".mp4", ".webm", ".ogg", ".mov", ".avi": ··· 238 239 if resp.Content != nil { 240 view.Contents = *resp.Content 241 + view.Lines = strings.Count(view.Contents, "\n") + 1 242 } 243 244 // with text, we may be dealing with markdown ··· 291 } 292 return slices.Contains(textualTypes, mimeType) 293 }
+4 -4
appview/repo/feed.go
··· 37 38 feed := &feeds.Feed{ 39 Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo), 40 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, ownerSlashRepo), Type: "text/html", Rel: "alternate"}, 41 Items: make([]*feeds.Item, 0), 42 Updated: time.UnixMilli(0), 43 } ··· 86 mainItem := &feeds.Item{ 87 Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 88 Description: description, 89 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId)}, 90 Created: pull.Created, 91 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 92 } ··· 100 roundItem := &feeds.Item{ 101 Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 102 Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo), 103 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId, round.RoundNumber)}, 104 Created: round.Created, 105 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 106 } ··· 124 return &feeds.Item{ 125 Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 126 Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo), 127 - Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, ownerSlashRepo, issue.IssueId)}, 128 Created: issue.Created, 129 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 130 }, nil
··· 37 38 feed := &feeds.Feed{ 39 Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo), 40 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.BaseUrl(), ownerSlashRepo), Type: "text/html", Rel: "alternate"}, 41 Items: make([]*feeds.Item, 0), 42 Updated: time.UnixMilli(0), 43 } ··· 86 mainItem := &feeds.Item{ 87 Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 88 Description: description, 89 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.BaseUrl(), ownerSlashRepo, pull.PullId)}, 90 Created: pull.Created, 91 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 92 } ··· 100 roundItem := &feeds.Item{ 101 Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 102 Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo), 103 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.BaseUrl(), ownerSlashRepo, pull.PullId, round.RoundNumber)}, 104 Created: round.Created, 105 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 106 } ··· 124 return &feeds.Item{ 125 Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 126 Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo), 127 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.BaseUrl(), ownerSlashRepo, issue.IssueId)}, 128 Created: issue.Created, 129 Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 130 }, nil
+7 -8
appview/settings/settings.go
··· 298 } 299 300 func (s *Settings) verifyUrl(did string, email string, code string) string { 301 - var appUrl string 302 - if s.Config.Core.Dev { 303 - appUrl = "http://" + s.Config.Core.ListenAddr 304 - } else { 305 - appUrl = s.Config.Core.AppviewHost 306 - } 307 - 308 - return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) 309 } 310 311 func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
··· 298 } 299 300 func (s *Settings) verifyUrl(did string, email string, code string) string { 301 + return fmt.Sprintf( 302 + "%s/settings/emails/verify?did=%s&email=%s&code=%s", 303 + s.Config.Core.BaseUrl(), 304 + url.QueryEscape(did), 305 + url.QueryEscape(email), 306 + url.QueryEscape(code), 307 + ) 308 } 309 310 func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) {
+1 -1
appview/state/knotstream.go
··· 122 if ce == nil { 123 continue 124 } 125 - if ce.Email == ke.Address || ce.Email == record.CommitterDid { 126 count += int(ce.Count) 127 } 128 }
··· 122 if ce == nil { 123 continue 124 } 125 + if ce.Email == ke.Address { 126 count += int(ce.Count) 127 } 128 }
+4 -4
appview/state/profile.go
··· 415 416 feed := feeds.Feed{ 417 Title: fmt.Sprintf("%s's timeline", author.Name), 418 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 419 Items: make([]*feeds.Item, 0), 420 Updated: time.UnixMilli(0), 421 Author: author, ··· 483 func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 484 return &feeds.Item{ 485 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 486 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 487 Created: pull.Created, 488 Author: author, 489 } ··· 492 func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 493 return &feeds.Item{ 494 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 495 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 496 Created: issue.Created, 497 Author: author, 498 } ··· 512 513 return &feeds.Item{ 514 Title: title, 515 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 516 Created: repo.Repo.Created, 517 Author: author, 518 }, nil
··· 415 416 feed := feeds.Feed{ 417 Title: fmt.Sprintf("%s's timeline", author.Name), 418 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.BaseUrl(), id.Handle), Type: "text/html", Rel: "alternate"}, 419 Items: make([]*feeds.Item, 0), 420 Updated: time.UnixMilli(0), 421 Author: author, ··· 483 func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 484 return &feeds.Item{ 485 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 486 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.BaseUrl(), owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 487 Created: pull.Created, 488 Author: author, 489 } ··· 492 func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 493 return &feeds.Item{ 494 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 495 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.BaseUrl(), owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 496 Created: issue.Created, 497 Author: author, 498 } ··· 512 513 return &feeds.Item{ 514 Title: title, 515 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.BaseUrl(), author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 516 Created: repo.Repo.Created, 517 Author: author, 518 }, nil
+3 -3
docs/DOCS.md
··· 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 ``` 377 378 - If you run a Linux distribution that uses systemd, you can 379 - use the provided service file to run the server. Copy 380 - [`knotserver.service`](https://tangled.org/tangled.org/core/blob/master/systemd/knotserver.service) 381 to `/etc/systemd/system/`. Then, run: 382 383 ```
··· 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 ``` 377 378 + If you run a Linux distribution that uses systemd, you can use the provided 379 + service file to run the server. Copy 380 + [`knotserver.service`](/systemd/knotserver.service) 381 to `/etc/systemd/system/`. Then, run: 382 383 ```
+3 -3
go.mod
··· 5 require ( 6 github.com/Blank-Xu/sql-adapter v1.1.1 7 github.com/alecthomas/assert/v2 v2.11.0 8 - github.com/alecthomas/chroma/v2 v2.23.1 9 github.com/avast/retry-go/v4 v4.6.1 10 github.com/blevesearch/bleve/v2 v2.5.3 11 github.com/bluekeyes/go-gitdiff v0.8.1 ··· 61 github.com/Microsoft/go-winio v0.6.2 // indirect 62 github.com/ProtonMail/go-crypto v1.3.0 // indirect 63 github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect 64 - github.com/alecthomas/repr v0.5.2 // indirect 65 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 66 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 67 github.com/aymerick/douceur v0.2.0 // indirect ··· 224 225 replace github.com/bluekeyes/go-gitdiff => tangled.sh/oppi.li/go-gitdiff v0.8.2 226 227 - replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.24.2 228 229 // from bluesky-social/indigo 230 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
··· 5 require ( 6 github.com/Blank-Xu/sql-adapter v1.1.1 7 github.com/alecthomas/assert/v2 v2.11.0 8 + github.com/alecthomas/chroma/v2 v2.15.0 9 github.com/avast/retry-go/v4 v4.6.1 10 github.com/blevesearch/bleve/v2 v2.5.3 11 github.com/bluekeyes/go-gitdiff v0.8.1 ··· 61 github.com/Microsoft/go-winio v0.6.2 // indirect 62 github.com/ProtonMail/go-crypto v1.3.0 // indirect 63 github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect 64 + github.com/alecthomas/repr v0.4.0 // indirect 65 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 66 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 67 github.com/aymerick/douceur v0.2.0 // indirect ··· 224 225 replace github.com/bluekeyes/go-gitdiff => tangled.sh/oppi.li/go-gitdiff v0.8.2 226 227 + replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.19.0 228 229 // from bluesky-social/indigo 230 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
+3 -4
go.sum
··· 13 github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= 14 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 15 github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 16 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 17 - github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= 18 - github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 19 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 20 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 21 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= ··· 413 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 414 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= 415 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= 416 - github.com/oppiliappan/chroma/v2 v2.24.2 h1:lHB9tWQxDoHa6sYEDdFep8SX6FPMmAF+ocGUffFwujE= 417 - github.com/oppiliappan/chroma/v2 v2.24.2/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= 418 github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo= 419 github.com/oppiliappan/go-git/v5 v5.17.0/go.mod h1:q/FE8C3SPMoRN7LoH9vRFiBzidAOBWJPS1CqVS8DN+w= 420 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
··· 13 github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= 14 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 15 github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 16 + github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 17 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 18 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 19 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 20 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= ··· 412 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 413 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= 414 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= 415 + github.com/oppiliappan/chroma/v2 v2.19.0 h1:PN7/pb+6JRKCva30NPTtRJMlrOyzgpPpIroNzy4ekHU= 416 + github.com/oppiliappan/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 417 github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo= 418 github.com/oppiliappan/go-git/v5 v5.17.0/go.mod h1:q/FE8C3SPMoRN7LoH9vRFiBzidAOBWJPS1CqVS8DN+w= 419 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+2 -2
input.css
··· 93 @apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 94 } 95 input { 96 - @apply p-3 border border-gray-100 block rounded bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-200 p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400;; 97 } 98 textarea { 99 - @apply border border-gray-100 block rounded bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-200 p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 100 } 101 details summary::-webkit-details-marker { 102 display: none;
··· 93 @apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 94 } 95 input { 96 + @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 97 } 98 textarea { 99 + @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 100 } 101 details summary::-webkit-details-marker { 102 display: none;
+4 -4
nix/gomod2nix.toml
··· 20 version = "v2.11.0" 21 hash = "sha256-tDJCDKZ0R4qNA7hgMKWrpDyogt1802LCJDBCExxdqaU=" 22 [mod."github.com/alecthomas/chroma/v2"] 23 - version = "v2.24.2" 24 - hash = "sha256-Xz4DLZpn98rwaLmNNztK3PJu9MVxDLSrhJI82ZzyFZo=" 25 replaced = "github.com/oppiliappan/chroma/v2" 26 [mod."github.com/alecthomas/repr"] 27 - version = "v0.5.2" 28 - hash = "sha256-PfIeyHh7xTbDN0g2otuDyUOQqbgS4KftVC1JKZ+6sdM=" 29 [mod."github.com/anmitsu/go-shlex"] 30 version = "v0.0.0-20200514113438-38f4b401e2be" 31 hash = "sha256-L3Ak4X2z7WXq7vMKuiHCOJ29nlpajUQ08Sfb9T0yP54="
··· 20 version = "v2.11.0" 21 hash = "sha256-tDJCDKZ0R4qNA7hgMKWrpDyogt1802LCJDBCExxdqaU=" 22 [mod."github.com/alecthomas/chroma/v2"] 23 + version = "v2.19.0" 24 + hash = "sha256-dxsu43a+PvHg2jYR0Tfys6a8x6IVR+9oCGAh+fvL3SM=" 25 replaced = "github.com/oppiliappan/chroma/v2" 26 [mod."github.com/alecthomas/repr"] 27 + version = "v0.4.0" 28 + hash = "sha256-CyAzMSTfLGHDtfGXi91y7XMVpPUDNOKjsznb+osl9dU=" 29 [mod."github.com/anmitsu/go-shlex"] 30 version = "v0.0.0-20200514113438-38f4b401e2be" 31 hash = "sha256-L3Ak4X2z7WXq7vMKuiHCOJ29nlpajUQ08Sfb9T0yP54="
+2 -2
nix/modules/appview.nix
··· 41 42 appviewHost = mkOption { 43 type = types.str; 44 - default = "https://tangled.org"; 45 - example = "https://example.com"; 46 description = "Public host URL for the appview instance"; 47 }; 48
··· 41 42 appviewHost = mkOption { 43 type = types.str; 44 + default = "tangled.org"; 45 + example = "example.com"; 46 description = "Public host URL for the appview instance"; 47 }; 48

History

5 rounds 11 comments
sign up or login to add to the discussion
2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
3/3 success
expand
expand 2 comments

@oppi.li makes sense. I completely forgot the indigo oauth behavior. I reverted the change.

lgtm, this bug is still present:

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org

but should be a quick fix, i can apply that on master!

pull request successfully merged
2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
expand 3 comments

when using dev, the callback URL should always be http://127.0.0.1/oauth/callback (or http://localhost), regardless of what the AppviewHost is set to. the port is ignored iirc.

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org (without the scheme).

i think BaseUrl is good naming choice. happy with that!

when using dev, the callback URL should always be http://127.0.0.1/oauth/callback (or http://localhost), regardless of what the AppviewHost is set to. the port is ignored iirc.

If someone set AppviewHost, I suspect they are testing on that host. I used to test on my own domain long ago. http://127.0.0.1:3000/oauth/callback is what was used in original code.

next, in appview/config/config.go, the default value for AppivewHost should be set to tangled.org

will do!

if you are using oauth.NewLocalhostConfig (as we do in dev), the redirect URL's host cannot be anything other than localhost or 127.0.0.1, if it is something else, you will get an error when performing the PAR request:

URL must use \"localhost\", \"127.0.0.1\" or \"[::1]\" as hostname at body.redirect_uri]"

when in dev, we should just ignore AppviewHost and use one of the predefined hosts. in the original code, the host is 127.0.0.1 which is one of the predefined hosts. the port is ignored anyway.

2 commits
expand
appview/pages/markup: smart commit autolink renderer
appview: strip scheme from CoreConfig.AppviewHost
3/3 success
expand
expand 5 comments
  • here, i think Url is a bit ambiguous to have on config, could use a better name here

rest of the changeset lgtm, will give this a test, thanks!

after some local testing, this seems to oauth, i am unable to login!

2026/01/21 06:25:06 WARN auth server request failed request=PAR statusCode=400 body="map[error:invalid_request error_description:Invalid authorization request: URL must use \"localhost\", \"127.0.0.1\" or \"[::1]\" as hostname at body.redirect_uri]"
2026/01/21 06:25:06 ERRO appview: failed to start auth handler=Login err="auth request failed: PAR request failed (HTTP 400): invalid_request"

@oppi.li I can't reproduce the oauth issue. Have you tried after setting TANGLED_APPVIEWHOST=127.0.0.1? you should remove the scheme now.

for Url() naming, would BaseUrl() be fine enough?

Also naming/consistency nit: tangledlink.gotangled_link.go? Keeping in line with reference_link.go. Rest looks OK!

@anirudh.fi I'm matching with extension/atlink.go. Doesn't reference_link.go violates the golang conventions? yes, I'm the one who wrote both... inconsistencies all over the place 🙈

1 commit
expand
appview/pages/markup: smart commit autolink renderer
3/3 success
expand
expand 1 comment

would be good to avoid hardcoding the appview host here!

1 commit
expand
appview/pages/markup: smart commit autolink renderer
1/3 failed, 2/3 success
expand
expand 0 comments