Monorepo for Tangled tangled.org

appview/issues: switch to discussions-style

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 045b2783 45c81020

verified
Changed files
+145 -11
appview
+4 -3
appview/ingester.go
··· 18 18 "tangled.sh/tangled.sh/core/appview/db" 19 19 "tangled.sh/tangled.sh/core/appview/pages/markup" 20 20 "tangled.sh/tangled.sh/core/appview/serververify" 21 + "tangled.sh/tangled.sh/core/appview/validator" 21 22 "tangled.sh/tangled.sh/core/idresolver" 22 23 "tangled.sh/tangled.sh/core/rbac" 23 24 ) ··· 28 29 IdResolver *idresolver.Resolver 29 30 Config *config.Config 30 31 Logger *slog.Logger 32 + Validator *validator.Validator 31 33 } 32 34 33 35 type processFunc func(ctx context.Context, e *models.Event) error ··· 875 877 return fmt.Errorf("failed to parse comment from record: %w", err) 876 878 } 877 879 878 - sanitizer := markup.NewSanitizer() 879 - if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { 880 - return fmt.Errorf("body is empty after HTML sanitization") 880 + if err := i.Validator.ValidateIssueComment(comment); err != nil { 881 + return fmt.Errorf("failed to validate comment: %w", err) 881 882 } 882 883 883 884 _, err = db.AddIssueComment(ddb, *comment)
+1 -1
appview/issues/issues.go
··· 53 53 db *db.DB, 54 54 config *config.Config, 55 55 notifier notify.Notifier, 56 + validator *validator.Validator, 56 57 ) *Issues { 57 58 return &Issues{ 58 59 oauth: oauth, ··· 102 103 Reactions: reactionCountMap, 103 104 UserReacted: userReactions, 104 105 }) 105 - 106 106 } 107 107 108 108 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
+2 -2
appview/pages/templates/layouts/repobase.html
··· 42 42 </section> 43 43 44 44 <section 45 - class="w-full flex flex-col drop-shadow-sm" 45 + class="w-full flex flex-col" 46 46 > 47 47 <nav class="w-full pl-4 overflow-auto"> 48 48 <div class="flex z-60"> ··· 81 81 </div> 82 82 </nav> 83 83 <section 84 - class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white" 84 + class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white" 85 85 > 86 86 {{ block "repoContent" . }}{{ end }} 87 87 </section>
+64
appview/pages/templates/repo/issues/fragments/commentList.html
··· 1 + {{ define "repo/issues/fragments/commentList" }} 2 + <div class="flex flex-col gap-8"> 3 + {{ range $item := .CommentList }} 4 + {{ template "commentListing" (list $ .) }} 5 + {{ end }} 6 + <div> 7 + {{ end }} 8 + 9 + {{ define "commentListing" }} 10 + {{ $root := index . 0 }} 11 + {{ $comment := index . 1 }} 12 + {{ $params := 13 + (dict 14 + "RepoInfo" $root.RepoInfo 15 + "LoggedInUser" $root.LoggedInUser 16 + "Issue" $root.Issue 17 + "Comment" $comment.Self) }} 18 + 19 + <div class="rounded border border-gray-300 dark:border-gray-700 w-full overflow-hidden shadow-sm"> 20 + {{ template "topLevelComment" $params }} 21 + 22 + <div class="relative ml-4 border-l border-gray-300 dark:border-gray-700"> 23 + {{ range $index, $reply := $comment.Replies }} 24 + <div class="relative "> 25 + <!-- Horizontal connector --> 26 + <div class="absolute left-0 top-6 w-4 h-px bg-gray-300 dark:bg-gray-700"></div> 27 + 28 + <div class="pl-2"> 29 + {{ 30 + template "replyComment" 31 + (dict 32 + "RepoInfo" $root.RepoInfo 33 + "LoggedInUser" $root.LoggedInUser 34 + "Issue" $root.Issue 35 + "Comment" $reply) 36 + }} 37 + </div> 38 + </div> 39 + {{ end }} 40 + </div> 41 + 42 + {{ if $root.LoggedInUser }} 43 + {{ template "repo/issues/fragments/replyIssueCommentPlaceholder" $params }} 44 + {{ else }} 45 + <div class="p-2 border-t border-gray-300 dark:border-gray-700 text-gray-500 dark:text-gray-400"> 46 + <a class="underline" href="/login">login</a> to reply to this discussion 47 + </div> 48 + {{ end }} 49 + </div> 50 + {{ end }} 51 + 52 + {{ define "topLevelComment" }} 53 + <div class="rounded px-6 py-4 bg-white dark:bg-gray-800"> 54 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 55 + {{ template "repo/issues/fragments/issueCommentBody" . }} 56 + </div> 57 + {{ end }} 58 + 59 + {{ define "replyComment" }} 60 + <div class="p-4 w-full mx-auto overflow-hidden"> 61 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 62 + {{ template "repo/issues/fragments/issueCommentBody" . }} 63 + </div> 64 + {{ end }}
+18
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
··· 1 + {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} 2 + <div class="p-2 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 + <img 4 + src="{{ tinyAvatar .LoggedInUser.Did }}" 5 + alt="" 6 + class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700" 7 + /> 8 + <input 9 + class="w-full py-2 border-none focus:outline-none" 10 + placeholder="Leave a reply..." 11 + hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 12 + hx-trigger="focus" 13 + hx-target="closest div" 14 + hx-swap="outerHTML" 15 + > 16 + </input> 17 + </div> 18 + {{ end }}
+2 -2
appview/pages/templates/repo/issues/issue.html
··· 9 9 {{ end }} 10 10 11 11 {{ define "repoContent" }} 12 - <header class="pb-4"> 12 + <header class="pb-2"> 13 13 <h1 class="text-2xl"> 14 14 {{ .Issue.Title | description }} 15 15 <span class="text-gray-500 dark:text-gray-400">#{{ .Issue.IssueId }}</span> ··· 39 39 </div> 40 40 41 41 {{ if .Issue.Body }} 42 - <article id="body" class="mt-8 prose dark:prose-invert"> 42 + <article id="body" class="mt-4 prose dark:prose-invert"> 43 43 {{ .Issue.Body | markdown }} 44 44 </article> 45 45 {{ end }}
+1 -1
appview/state/router.go
··· 232 232 } 233 233 234 234 func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler { 235 - issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier) 235 + issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.notifier, s.validator) 236 236 return issues.Router(mw) 237 237 } 238 238
+5 -2
appview/state/state.go
··· 28 28 "tangled.sh/tangled.sh/core/appview/pages" 29 29 posthogService "tangled.sh/tangled.sh/core/appview/posthog" 30 30 "tangled.sh/tangled.sh/core/appview/reporesolver" 31 + "tangled.sh/tangled.sh/core/appview/validator" 31 32 xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient" 32 33 "tangled.sh/tangled.sh/core/eventconsumer" 33 34 "tangled.sh/tangled.sh/core/idresolver" ··· 53 54 knotstream *eventconsumer.Consumer 54 55 spindlestream *eventconsumer.Consumer 55 56 logger *slog.Logger 57 + validator *validator.Validator 56 58 } 57 59 58 60 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 73 75 } 74 76 75 77 pgs := pages.NewPages(config, res) 76 - 77 78 cache := cache.New(config.Redis.Addr) 78 79 sess := session.New(cache) 79 - 80 80 oauth := oauth.NewOAuth(config, sess) 81 + validator := validator.New(d) 81 82 82 83 posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) 83 84 if err != nil { ··· 121 122 IdResolver: res, 122 123 Config: config, 123 124 Logger: tlog.New("ingester"), 125 + Validator: validator, 124 126 } 125 127 err = jc.StartJetstream(ctx, ingester.Ingest()) 126 128 if err != nil { ··· 160 162 knotstream, 161 163 spindlestream, 162 164 slog.Default(), 165 + validator, 163 166 } 164 167 165 168 return state, nil
+35
appview/validator/issue.go
··· 1 + package validator 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + "tangled.sh/tangled.sh/core/appview/db" 8 + "tangled.sh/tangled.sh/core/appview/pages/markup" 9 + ) 10 + 11 + func (v *Validator) ValidateIssueComment(comment *db.IssueComment) error { 12 + // if comments have parents, only ingest ones that are 1 level deep 13 + if comment.ReplyTo != nil { 14 + parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) 15 + if err != nil { 16 + return fmt.Errorf("failed to fetch parent comment: %w", err) 17 + } 18 + if len(parents) != 1 { 19 + return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 20 + } 21 + 22 + // depth check 23 + parent := parents[0] 24 + if parent.ReplyTo != nil { 25 + return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 26 + } 27 + } 28 + 29 + sanitizer := markup.NewSanitizer() 30 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { 31 + return fmt.Errorf("body is empty after HTML sanitization") 32 + } 33 + 34 + return nil 35 + }
+13
appview/validator/validator.go
··· 1 + package validator 2 + 3 + import "tangled.sh/tangled.sh/core/appview/db" 4 + 5 + type Validator struct { 6 + db *db.DB 7 + } 8 + 9 + func New(db *db.DB) *Validator { 10 + return &Validator{ 11 + db: db, 12 + } 13 + }