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