Yōten: A social tracker for your language learning journey built on the atproto.

feat(comments): allow user to edit comment

brookjeynes.dev 2b992a31 1bd45ce8

verified
+190 -9
+127 -1
internal/server/handlers/comment.go
··· 3 3 import ( 4 4 "log" 5 5 "net/http" 6 + "strings" 6 7 "time" 7 8 8 9 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 54 55 } 55 56 56 57 commentBody := r.FormValue("comment") 57 - if len(commentBody) == 0 { 58 + if len(strings.TrimSpace(commentBody)) == 0 { 58 59 log.Println("invalid comment form: missing comment body") 59 60 htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 60 61 return ··· 191 192 w.WriteHeader(http.StatusOK) 192 193 } 193 194 } 195 + 196 + func (h *Handler) HandleEditCommentPage(w http.ResponseWriter, r *http.Request) { 197 + user, err := bsky.GetUserWithBskyProfile(h.Oauth, r) 198 + if err != nil { 199 + log.Println("failed to get logged-in user:", err) 200 + htmx.HxRedirect(w, "/login") 201 + return 202 + } 203 + 204 + rkey := chi.URLParam(r, "rkey") 205 + comment, err := db.GetCommentByRkey(h.Db, user.Did, rkey) 206 + if err != nil { 207 + log.Println("failed to get comment from db:", err) 208 + htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 209 + return 210 + } 211 + 212 + // TODO: Get comment replies. 213 + 214 + if user.Did != comment.Did { 215 + log.Printf("user '%s' does not own record '%s'", user.Did, rkey) 216 + htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this comment.") 217 + return 218 + } 219 + 220 + switch r.Method { 221 + case http.MethodGet: 222 + partials.EditComment(partials.EditCommentProps{Comment: comment}).Render(r.Context(), w) 223 + case http.MethodPost: 224 + client, err := h.Oauth.AuthorizedClient(r, w) 225 + if err != nil { 226 + log.Println("failed to get authorized client:", err) 227 + htmx.HxRedirect(w, "/login") 228 + return 229 + } 230 + 231 + profile, err := db.GetProfile(h.Db, user.Did) 232 + if err != nil { 233 + log.Println("failed to get logged-in user:", err) 234 + htmx.HxRedirect(w, "/login") 235 + return 236 + } 237 + 238 + err = r.ParseForm() 239 + if err != nil { 240 + log.Println("invalid comment form:", err) 241 + htmx.HxError(w, http.StatusBadRequest, "Unable to process comment, please try again later.") 242 + return 243 + } 244 + 245 + commentBody := r.FormValue("comment") 246 + if len(strings.TrimSpace(commentBody)) == 0 { 247 + log.Println("invalid comment form: missing comment body") 248 + htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.") 249 + return 250 + } 251 + 252 + updatedComment := db.Comment{ 253 + Rkey: comment.Rkey, 254 + Did: comment.Did, 255 + StudySessionUri: comment.StudySessionUri, 256 + Body: commentBody, 257 + CreatedAt: comment.CreatedAt, 258 + } 259 + 260 + ex, _ := client.RepoGetRecord(r.Context(), "", yoten.FeedCommentNSID, user.Did, updatedComment.Rkey) 261 + var cid *string 262 + if ex != nil { 263 + cid = ex.Cid 264 + } 265 + 266 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 267 + Collection: yoten.FeedCommentNSID, 268 + Repo: updatedComment.Did, 269 + Rkey: updatedComment.Rkey, 270 + Record: &lexutil.LexiconTypeDecoder{ 271 + Val: &yoten.FeedComment{ 272 + LexiconTypeID: yoten.FeedCommentNSID, 273 + Body: updatedComment.Body, 274 + Subject: updatedComment.StudySessionUri.String(), 275 + CreatedAt: updatedComment.CreatedAt.Format(time.RFC3339), 276 + }, 277 + }, 278 + SwapRecord: cid, 279 + }) 280 + if err != nil { 281 + log.Println("failed to update study session record:", err) 282 + htmx.HxError(w, http.StatusInternalServerError, "Failed to update comment, try again later.") 283 + return 284 + } 285 + 286 + if !h.Config.Core.Dev { 287 + event := posthog.Capture{ 288 + DistinctId: user.Did, 289 + Event: ph.CommentRecordEditedEvent, 290 + Properties: posthog.NewProperties(). 291 + Set("is_reply", updatedComment.ParentCommentUri != nil). 292 + Set("character_count", len(updatedComment.Body)). 293 + Set("study_session_uri", updatedComment.StudySessionUri.String()), 294 + } 295 + 296 + if updatedComment.ParentCommentUri != nil { 297 + event.Properties.Set("parent_comment_uri", *updatedComment.ParentCommentUri) 298 + } 299 + 300 + err = h.Posthog.Enqueue(event) 301 + if err != nil { 302 + log.Println("failed to enqueue posthog event:", err) 303 + } 304 + } 305 + 306 + partials.Comment(partials.CommentProps{ 307 + Comment: db.CommentFeedItem{ 308 + CommentWithBskyProfile: db.CommentWithBskyProfile{ 309 + Comment: updatedComment, 310 + ProfileLevel: profile.Level, 311 + ProfileDisplayName: profile.DisplayName, 312 + BskyProfile: user.BskyProfile, 313 + }, 314 + // TODO 315 + Replies: []db.CommentWithBskyProfile{}, 316 + }, 317 + }).Render(r.Context(), w) 318 + } 319 + }
+2
internal/server/handlers/router.go
··· 89 89 r.Route("/comment", func(r chi.Router) { 90 90 r.Use(middleware.AuthMiddleware(h.Oauth)) 91 91 r.Post("/new", h.HandleNewComment) 92 + r.Get("/edit/{rkey}", h.HandleEditCommentPage) 93 + r.Post("/edit/{rkey}", h.HandleEditCommentPage) 92 94 r.Delete("/{rkey}", h.HandleDeleteComment) 93 95 }) 94 96
+13 -8
internal/server/views/partials/comment.templ
··· 62 62 <p class="text-text-muted text-sm">&commat;{ params.Comment.BskyProfile.Handle }</p> 63 63 </div> 64 64 </div> 65 + // TODO: Only show on comments you own 65 66 <details class="relative inline-block text-left"> 66 67 <summary class="cursor-pointer list-none"> 67 68 <div class="btn btn-muted p-2"> ··· 69 70 </div> 70 71 </summary> 71 72 <div class="absolute flex flex-col right-0 mt-2 p-1 gap-1 rounded w-32 bg-bg-light border border-bg-dark"> 72 - <button id="edit-button" type="button" class="w-full"> 73 - <a 74 - href={ templ.URL(fmt.Sprintf("/comment/edit/%s", params.Comment.Rkey)) } 75 - class="text-base text-text flex items-center px-4 py-2 text-sm hover:bg-bg gap-2" 76 - > 77 - <i class="w-4 h-4" data-lucide="square-pen"></i> 78 - Edit 79 - </a> 73 + <button 74 + class="text-base cursor-pointer flex items-center px-4 py-2 text-sm hover:bg-bg gap-2 group" 75 + type="button" 76 + id="edit-button" 77 + hx-disabled-elt="#delete-button,#edit-button" 78 + hx-target={ "#" + elementId } 79 + hx-swap="outerSelf" 80 + hx-get={ templ.URL(fmt.Sprintf("/comment/edit/%s", params.Comment.Rkey)) } 81 + > 82 + <i class="w-4 h-4" data-lucide="square-pen"></i> 83 + Edit 84 + <i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i> 80 85 </button> 81 86 <button 82 87 class="text-base text-red-600 cursor-pointer flex items-center px-4 py-2 text-sm hover:bg-bg gap-2 group"
+44
internal/server/views/partials/edit-comment.templ
··· 1 + package partials 2 + 3 + import "fmt" 4 + 5 + templ EditComment(params EditCommentProps) { 6 + {{ elementId := SanitiseHtmlId(fmt.Sprintf("comment-%s-%s", params.Comment.Did, params.Comment.Rkey)) }} 7 + <div id={ elementId } class="flex flex-col gap-3" x-init="lucide.createIcons()"> 8 + <form 9 + hx-post={ templ.SafeURL("/comment/edit/" + params.Comment.Rkey) } 10 + hx-target={ "#" + elementId } 11 + hx-swap="outerSelf" 12 + hx-disabled-elt="#update-comment-button,#cancel-comment-button" 13 + x-data="{ text: '' }" 14 + x-init="text = $el.querySelector('textarea').value" 15 + > 16 + <div class="mt-2"> 17 + <textarea 18 + x-model="text" 19 + id="comment" 20 + name="comment" 21 + placeholder="Share your thoughts about this study session..." 22 + class="input w-full" 23 + maxLength="256" 24 + rows="3" 25 + > 26 + { params.Comment.Body } 27 + </textarea> 28 + <div class="flex justify-between mt-2"> 29 + <div class="text-sm text-text-muted"> 30 + <span x-text="text.length"></span> / 256 31 + </div> 32 + <div class="flex items-center gap-2"> 33 + <button type="submit" id="update-comment-button" class="btn btn-primary w-fit"> 34 + Update Comment 35 + </button> 36 + <button id="cancel-comment-button" class="btn btn-muted w-fit"> 37 + Cancel 38 + </button> 39 + </div> 40 + </div> 41 + </div> 42 + </form> 43 + </div> 44 + }
+4
internal/server/views/partials/partials.go
··· 227 227 type CommentProps struct { 228 228 Comment db.CommentFeedItem 229 229 } 230 + 231 + type EditCommentProps struct { 232 + Comment db.Comment 233 + }