tangled
alpha
login
or
join now
yoten.app
/
yoten
17
fork
atom
Yōten: A social tracker for your language learning journey built on the atproto.
17
fork
atom
overview
issues
pulls
pipelines
feat(comments): allow user to edit comment
brookjeynes.dev
5 months ago
e97cc128
825b488f
verified
This commit was signed with the committer's
known signature
.
brookjeynes.dev
SSH Key Fingerprint:
SHA256:N3n3PCBSiXfS6EHlmGdx+LMEruJMj6FS2hqaXyfsw0s=
+190
-9
5 changed files
expand all
collapse all
unified
split
internal
server
handlers
comment.go
router.go
views
partials
comment.templ
edit-comment.templ
partials.go
+127
-1
internal/server/handlers/comment.go
···
3
import (
4
"log"
5
"net/http"
0
6
"time"
7
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
54
}
55
56
commentBody := r.FormValue("comment")
57
-
if len(commentBody) == 0 {
58
log.Println("invalid comment form: missing comment body")
59
htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.")
60
return
···
191
w.WriteHeader(http.StatusOK)
192
}
193
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
3
import (
4
"log"
5
"net/http"
6
+
"strings"
7
"time"
8
9
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
55
}
56
57
commentBody := r.FormValue("comment")
58
+
if len(strings.TrimSpace(commentBody)) == 0 {
59
log.Println("invalid comment form: missing comment body")
60
htmx.HxError(w, http.StatusBadRequest, "Comment cannot be empty.")
61
return
···
192
w.WriteHeader(http.StatusOK)
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
r.Route("/comment", func(r chi.Router) {
90
r.Use(middleware.AuthMiddleware(h.Oauth))
91
r.Post("/new", h.HandleNewComment)
0
0
92
r.Delete("/{rkey}", h.HandleDeleteComment)
93
})
94
···
89
r.Route("/comment", func(r chi.Router) {
90
r.Use(middleware.AuthMiddleware(h.Oauth))
91
r.Post("/new", h.HandleNewComment)
92
+
r.Get("/edit/{rkey}", h.HandleEditCommentPage)
93
+
r.Post("/edit/{rkey}", h.HandleEditCommentPage)
94
r.Delete("/{rkey}", h.HandleDeleteComment)
95
})
96
+13
-8
internal/server/views/partials/comment.templ
···
62
<p class="text-text-muted text-sm">@{ params.Comment.BskyProfile.Handle }</p>
63
</div>
64
</div>
0
65
<details class="relative inline-block text-left">
66
<summary class="cursor-pointer list-none">
67
<div class="btn btn-muted p-2">
···
69
</div>
70
</summary>
71
<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>
0
0
0
0
80
</button>
81
<button
82
class="text-base text-red-600 cursor-pointer flex items-center px-4 py-2 text-sm hover:bg-bg gap-2 group"
···
62
<p class="text-text-muted text-sm">@{ params.Comment.BskyProfile.Handle }</p>
63
</div>
64
</div>
65
+
// TODO: Only show on comments you own
66
<details class="relative inline-block text-left">
67
<summary class="cursor-pointer list-none">
68
<div class="btn btn-muted p-2">
···
70
</div>
71
</summary>
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">
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>
85
</button>
86
<button
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
type CommentProps struct {
228
Comment db.CommentFeedItem
229
}
0
0
0
0
···
227
type CommentProps struct {
228
Comment db.CommentFeedItem
229
}
230
+
231
+
type EditCommentProps struct {
232
+
Comment db.Comment
233
+
}