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: fetch study session comments
brookjeynes.dev
5 months ago
7aed43aa
b3033c41
verified
This commit was signed with the committer's
known signature
.
brookjeynes.dev
SSH Key Fingerprint:
SHA256:N3n3PCBSiXfS6EHlmGdx+LMEruJMj6FS2hqaXyfsw0s=
+358
-59
12 changed files
expand all
collapse all
unified
split
internal
db
comment.go
utils.go
server
handlers
comment.go
study-session.go
views
partials
comment-feed.templ
comment.templ
discussion.templ
edit-comment.templ
partials.go
reactions.templ
study-session.templ
study-session.templ
+128
internal/db/comment.go
···
3
import (
4
"database/sql"
5
"fmt"
0
6
"time"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
20
ProfileLevel int
21
ProfileDisplayName string
22
BskyProfile types.BskyProfile
0
0
0
0
0
0
23
}
24
25
type Comment struct {
···
119
120
return comment, nil
121
}
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
"database/sql"
5
"fmt"
6
+
"sort"
7
"time"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
21
ProfileLevel int
22
ProfileDisplayName string
23
BskyProfile types.BskyProfile
24
+
}
25
+
26
+
type CommentWithLocalProfile struct {
27
+
Comment
28
+
ProfileLevel int
29
+
ProfileDisplayName string
30
}
31
32
type Comment struct {
···
126
127
return comment, nil
128
}
129
+
130
+
func GetCommentsForSession(e Execer, studySessionUri string, limit, offset int) ([]CommentWithLocalProfile, error) {
131
+
topLevelCommentsQuery := `
132
+
select
133
+
c.id, c.did, c.rkey, c.study_session_uri, c.parent_comment_uri,
134
+
c.body, c.is_deleted, c.created_at,
135
+
p.display_name, p.level
136
+
from comments c
137
+
join profiles p on c.did = p.did
138
+
where c.study_session_uri = ? and c.parent_comment_uri is null
139
+
order by c.created_at asc
140
+
limit ? offset ?;
141
+
`
142
+
rows, err := e.Query(topLevelCommentsQuery, studySessionUri, limit, offset)
143
+
if err != nil {
144
+
return nil, fmt.Errorf("failed to query top-level comments: %w", err)
145
+
}
146
+
defer rows.Close()
147
+
148
+
allCommentsMap := make(map[string]CommentWithLocalProfile)
149
+
var topLevelCommentUris []string
150
+
151
+
for rows.Next() {
152
+
comment, err := scanCommentWithLocalProfile(rows)
153
+
if err != nil {
154
+
return nil, err
155
+
}
156
+
allCommentsMap[comment.CommentAt().String()] = comment
157
+
topLevelCommentUris = append(topLevelCommentUris, comment.CommentAt().String())
158
+
}
159
+
if err = rows.Err(); err != nil {
160
+
return nil, fmt.Errorf("error iterating top-level comment rows: %w", err)
161
+
}
162
+
rows.Close()
163
+
164
+
if len(topLevelCommentUris) == 0 {
165
+
return []CommentWithLocalProfile{}, nil
166
+
}
167
+
168
+
repliesQuery := `
169
+
select
170
+
c.id, c.did, c.rkey, c.study_session_uri, c.parent_comment_uri,
171
+
c.body, c.is_deleted, c.created_at,
172
+
p.display_name, p.level
173
+
from comments c
174
+
join profiles p on c.did = p.did
175
+
where c.study_session_uri = ? and c.parent_comment_uri in (` + GetPlaceholders(len(topLevelCommentUris)) + `);
176
+
`
177
+
args := make([]any, len(topLevelCommentUris)+1)
178
+
args[0] = studySessionUri
179
+
for i, uri := range topLevelCommentUris {
180
+
args[i+1] = uri
181
+
}
182
+
183
+
replyRows, err := e.Query(repliesQuery, args...)
184
+
if err != nil {
185
+
return nil, fmt.Errorf("failed to query replies: %w", err)
186
+
}
187
+
defer replyRows.Close()
188
+
189
+
for replyRows.Next() {
190
+
reply, err := scanCommentWithLocalProfile(replyRows)
191
+
if err != nil {
192
+
return nil, err
193
+
}
194
+
allCommentsMap[reply.CommentAt().String()] = reply
195
+
}
196
+
if err = replyRows.Err(); err != nil {
197
+
return nil, fmt.Errorf("error iterating reply rows: %w", err)
198
+
}
199
+
200
+
finalComments := make([]CommentWithLocalProfile, 0, len(allCommentsMap))
201
+
for _, comment := range allCommentsMap {
202
+
finalComments = append(finalComments, comment)
203
+
}
204
+
205
+
sort.Slice(finalComments, func(i, j int) bool {
206
+
return finalComments[i].CreatedAt.Before(finalComments[j].CreatedAt)
207
+
})
208
+
209
+
return finalComments, nil
210
+
}
211
+
212
+
func scanCommentWithLocalProfile(rows *sql.Rows) (CommentWithLocalProfile, error) {
213
+
var comment CommentWithLocalProfile
214
+
var parentUri sql.NullString
215
+
var studySessionUriStr string
216
+
var createdAtStr string
217
+
218
+
err := rows.Scan(
219
+
&comment.ID, &comment.Did, &comment.Rkey, &studySessionUriStr,
220
+
&parentUri, &comment.Body, &comment.IsDeleted, &createdAtStr,
221
+
&comment.ProfileDisplayName, &comment.ProfileLevel,
222
+
)
223
+
if err != nil {
224
+
return CommentWithLocalProfile{}, fmt.Errorf("failed to scan comment row: %w", err)
225
+
}
226
+
227
+
comment.CreatedAt, err = time.Parse(time.RFC3339, createdAtStr)
228
+
if err != nil {
229
+
return CommentWithLocalProfile{}, fmt.Errorf("failed to parse created at string '%s': %w", createdAtStr, err)
230
+
}
231
+
232
+
parsedStudySessionUri, err := syntax.ParseATURI(studySessionUriStr)
233
+
if err != nil {
234
+
return CommentWithLocalProfile{}, fmt.Errorf("failed to parse at-uri: %w", err)
235
+
}
236
+
comment.StudySessionUri = parsedStudySessionUri
237
+
238
+
if parentUri.Valid {
239
+
parsedParentUri, err := syntax.ParseATURI(parentUri.String)
240
+
if err != nil {
241
+
return CommentWithLocalProfile{}, fmt.Errorf("failed to parse at-uri: %w", err)
242
+
}
243
+
comment.ParentCommentUri = &parsedParentUri
244
+
}
245
+
246
+
comment.CreatedAt, _ = time.Parse(time.RFC3339, createdAtStr)
247
+
248
+
return comment, nil
249
+
}
+10
internal/db/utils.go
···
1
package db
2
3
import (
0
0
4
"golang.org/x/text/cases"
5
"golang.org/x/text/language"
6
)
···
14
titleStr := caser.String(str)
15
return titleStr
16
}
0
0
0
0
0
0
0
0
···
1
package db
2
3
import (
4
+
"strings"
5
+
6
"golang.org/x/text/cases"
7
"golang.org/x/text/language"
8
)
···
16
titleStr := caser.String(str)
17
return titleStr
18
}
19
+
20
+
// Generates `?, ?, ?` for SQL IN clauses.
21
+
func GetPlaceholders(count int) string {
22
+
if count < 1 {
23
+
return ""
24
+
}
25
+
return strings.Repeat("?,", count-1) + "?"
26
+
}
+59
-8
internal/server/handlers/comment.go
···
18
"yoten.app/internal/db"
19
"yoten.app/internal/server/htmx"
20
"yoten.app/internal/server/views/partials"
21
-
)
22
-
23
-
const (
24
-
PendingCommentCreation string = "pending_comment_creation"
25
-
PendingCommentDeletion string = "pending_comment_deletion"
26
)
27
28
func (h *Handler) HandleNewComment(w http.ResponseWriter, r *http.Request) {
···
125
},
126
Replies: []db.CommentWithBskyProfile{},
127
},
0
128
}).Render(r.Context(), w)
129
}
130
···
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.")
···
311
ProfileDisplayName: profile.DisplayName,
312
BskyProfile: user.BskyProfile,
313
},
314
-
// TODO
0
315
Replies: []db.CommentWithBskyProfile{},
316
},
0
317
}).Render(r.Context(), w)
318
}
319
}
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
···
18
"yoten.app/internal/db"
19
"yoten.app/internal/server/htmx"
20
"yoten.app/internal/server/views/partials"
21
+
"yoten.app/internal/types"
22
+
"yoten.app/internal/utils"
0
0
0
23
)
24
25
func (h *Handler) HandleNewComment(w http.ResponseWriter, r *http.Request) {
···
122
},
123
Replies: []db.CommentWithBskyProfile{},
124
},
125
+
DoesOwn: true,
126
}).Render(r.Context(), w)
127
}
128
···
207
return
208
}
209
0
0
210
if user.Did != comment.Did {
211
log.Printf("user '%s' does not own record '%s'", user.Did, rkey)
212
htmx.HxError(w, http.StatusUnauthorized, "You do not have permissions to edit this comment.")
···
307
ProfileDisplayName: profile.DisplayName,
308
BskyProfile: user.BskyProfile,
309
},
310
+
// Replies are not needed to be populated as this response will
311
+
// replace just the edited comment.
312
Replies: []db.CommentWithBskyProfile{},
313
},
314
+
DoesOwn: true,
315
}).Render(r.Context(), w)
316
}
317
}
318
+
319
+
func (h *Handler) BuildCommentFeed(comments []db.CommentWithLocalProfile) ([]db.CommentFeedItem, error) {
320
+
authorDids := utils.Map(comments, func(comment db.CommentWithLocalProfile) string {
321
+
return comment.Did
322
+
})
323
+
bskyProfiles, err := bsky.GetBskyProfiles(authorDids)
324
+
if err != nil {
325
+
return []db.CommentFeedItem{}, err
326
+
}
327
+
328
+
return assembleCommentFeed(comments, bskyProfiles), nil
329
+
}
330
+
331
+
func assembleCommentFeed(localComments []db.CommentWithLocalProfile, bskyProfiles map[string]types.BskyProfile) []db.CommentFeedItem {
332
+
hydratedComments := make(map[string]db.CommentWithBskyProfile)
333
+
repliesMap := make(map[string][]db.CommentWithBskyProfile)
334
+
335
+
for _, lc := range localComments {
336
+
hydrated := db.CommentWithBskyProfile{
337
+
Comment: lc.Comment,
338
+
ProfileDisplayName: lc.ProfileDisplayName,
339
+
ProfileLevel: lc.ProfileLevel,
340
+
}
341
+
if profile, ok := bskyProfiles[lc.Did]; ok {
342
+
hydrated.BskyProfile = profile
343
+
}
344
+
hydratedComments[lc.CommentAt().String()] = hydrated
345
+
}
346
+
347
+
var topLevelComments []db.CommentWithBskyProfile
348
+
for _, hydrated := range hydratedComments {
349
+
if hydrated.ParentCommentUri == nil {
350
+
topLevelComments = append(topLevelComments, hydrated)
351
+
} else {
352
+
parentURI := hydrated.ParentCommentUri.String()
353
+
repliesMap[parentURI] = append(repliesMap[parentURI], hydrated)
354
+
}
355
+
}
356
+
357
+
var feed []db.CommentFeedItem
358
+
for _, topLevel := range topLevelComments {
359
+
feedItem := db.CommentFeedItem{
360
+
CommentWithBskyProfile: topLevel,
361
+
Replies: []db.CommentWithBskyProfile{},
362
+
}
363
+
if replies, ok := repliesMap[topLevel.CommentAt().String()]; ok {
364
+
feedItem.Replies = replies
365
+
}
366
+
feed = append(feed, feedItem)
367
+
}
368
+
369
+
return feed
370
+
}
+61
internal/server/handlers/study-session.go
···
10
11
comatproto "github.com/bluesky-social/indigo/api/atproto"
12
"github.com/bluesky-social/indigo/atproto/identity"
0
13
lexutil "github.com/bluesky-social/indigo/lex/util"
14
"github.com/go-chi/chi/v5"
15
"github.com/posthog/posthog-go"
···
687
}
688
689
func (h *Handler) HandleStudySessionPageCommentFeed(w http.ResponseWriter, r *http.Request) {
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
690
}
···
10
11
comatproto "github.com/bluesky-social/indigo/api/atproto"
12
"github.com/bluesky-social/indigo/atproto/identity"
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
lexutil "github.com/bluesky-social/indigo/lex/util"
15
"github.com/go-chi/chi/v5"
16
"github.com/posthog/posthog-go"
···
688
}
689
690
func (h *Handler) HandleStudySessionPageCommentFeed(w http.ResponseWriter, r *http.Request) {
691
+
user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r)
692
+
693
+
didOrHandle := chi.URLParam(r, "user")
694
+
if didOrHandle == "" {
695
+
http.Error(w, "Bad request", http.StatusBadRequest)
696
+
return
697
+
}
698
+
699
+
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
700
+
if !ok {
701
+
w.WriteHeader(http.StatusNotFound)
702
+
views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w)
703
+
return
704
+
}
705
+
rkey := chi.URLParam(r, "rkey")
706
+
studySessionUri := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", ident.DID.String(), yoten.FeedSessionNSID, rkey))
707
+
708
+
pageStr := r.URL.Query().Get("page")
709
+
if pageStr == "" {
710
+
pageStr = "1"
711
+
}
712
+
page, err := strconv.ParseInt(pageStr, 10, 64)
713
+
if err != nil {
714
+
log.Println("failed to parse page value:", err)
715
+
page = 1
716
+
}
717
+
if page == 0 {
718
+
page = 1
719
+
}
720
+
721
+
const pageSize = 2
722
+
offset := (page - 1) * pageSize
723
+
724
+
commentFeed, err := db.GetCommentsForSession(h.Db, studySessionUri.String(), pageSize+1, int(offset))
725
+
if err != nil {
726
+
log.Println("failed to get comment feed:", err)
727
+
htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.")
728
+
return
729
+
}
730
+
731
+
nextPage := 0
732
+
if len(commentFeed) > pageSize {
733
+
nextPage = int(page + 1)
734
+
commentFeed = commentFeed[:pageSize]
735
+
}
736
+
737
+
populatedCommentFeed, err := h.BuildCommentFeed(commentFeed)
738
+
if err != nil {
739
+
log.Println("failed to populate comment feed:", err)
740
+
htmx.HxError(w, http.StatusInternalServerError, "Failed to get comment feed, try again later.")
741
+
return
742
+
}
743
+
744
+
partials.CommentFeed(partials.CommentFeedProps{
745
+
Feed: populatedCommentFeed,
746
+
NextPage: nextPage,
747
+
User: user,
748
+
StudySessionDid: ident.DID.String(),
749
+
StudySessionRkey: rkey,
750
+
}).Render(r.Context(), w)
751
}
+31
internal/server/views/partials/comment-feed.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
···
1
+
package partials
2
+
3
+
import "fmt"
4
+
5
+
templ CommentFeed(params CommentFeedProps) {
6
+
for _, comment := range params.Feed {
7
+
{{
8
+
isSelf := false
9
+
if params.User != nil {
10
+
isSelf = params.User.Did == comment.Did
11
+
}
12
+
}}
13
+
@Comment(CommentProps{
14
+
Comment: comment,
15
+
DoesOwn: isSelf,
16
+
})
17
+
}
18
+
if params.NextPage > 0 {
19
+
<div
20
+
id="next-feed-segment"
21
+
hx-get={ templ.SafeURL(fmt.Sprintf("/%s/session/%s/feed?page=%d", params.StudySessionDid,
22
+
params.StudySessionRkey, params.NextPage)) }
23
+
hx-trigger="revealed"
24
+
hx-swap="outerHTML"
25
+
>
26
+
<div class="flex justify-center py-4">
27
+
<i data-lucide="loader-circle" class="w-6 h-6 animate-spin text-text-muted"></i>
28
+
</div>
29
+
</div>
30
+
}
31
+
}
+36
-35
internal/server/views/partials/comment.templ
···
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">
69
-
<i class="w-4 h-4 flex-shrink-0" data-lucide="ellipsis"></i>
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
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"
88
-
type="button"
89
-
id="delete-button"
90
-
hx-disabled-elt="#delete-button,#edit-button"
91
-
hx-target={ "#" + elementId }
92
-
hx-swap="outerSelf"
93
-
hx-delete={ templ.URL(fmt.Sprintf("/comment/%s", params.Comment.Rkey)) }
94
-
>
95
-
<i class="w-4 h-4" data-lucide="trash-2"></i>
96
-
Delete
97
-
<i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i>
98
-
</button>
99
-
</div>
100
-
</details>
101
</div>
102
<p class="leading-relaxed break-words">
103
{ params.Comment.Body }
···
62
<p class="text-text-muted text-sm">@{ params.Comment.BskyProfile.Handle }</p>
63
</div>
64
</div>
65
+
if params.DoesOwn {
66
+
<details class="relative inline-block text-left">
67
+
<summary class="cursor-pointer list-none">
68
+
<div class="btn btn-muted p-2">
69
+
<i class="w-4 h-4 flex-shrink-0" data-lucide="ellipsis"></i>
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="btn hover:bg-bg group justify-start px-2"
75
+
type="button"
76
+
id="edit-button"
77
+
hx-disabled-elt="#delete-button,#edit-button"
78
+
hx-target={ "#" + elementId }
79
+
hx-swap="outerHTML"
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
+
<span class="text-sm">Edit</span>
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="btn text-red-600 hover:bg-bg group justify-start px-2"
88
+
type="button"
89
+
id="delete-button"
90
+
hx-disabled-elt="#delete-button,#edit-button"
91
+
hx-target={ "#" + elementId }
92
+
hx-swap="outerHTML"
93
+
hx-delete={ templ.URL(fmt.Sprintf("/comment/%s", params.Comment.Rkey)) }
94
+
>
95
+
<i class="w-4 h-4" data-lucide="trash-2"></i>
96
+
<span class="text-sm">Delete</span>
97
+
<i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i>
98
+
</button>
99
</div>
100
+
</details>
101
+
}
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
102
</div>
103
<p class="leading-relaxed break-words">
104
{ params.Comment.Body }
+14
-10
internal/server/views/partials/discussion.templ
···
1
package partials
2
0
0
3
templ Discussion(params DiscussionProps) {
4
<div class="card">
5
<div class="flex items-center gap-2">
···
9
<form
10
hx-post="/comment/new"
11
hx-swap="afterbegin"
12
-
hx-target="#comments"
13
hx-disabled-elt="#post-comment-button"
14
@htmx:after-request="text = ''"
15
x-data="{ text: '' }"
···
29
<div class="text-sm text-text-muted">
30
<span x-text="text.length"></span> / 256
31
</div>
32
-
<button
33
-
type="submit"
34
-
id="post-comment-button"
35
-
class="btn btn-primary w-fit"
36
-
>
37
Post Comment
38
</button>
39
</div>
40
</div>
41
</form>
42
-
<div id="comments" class="flex flex-col gap-4 mt-2">
43
-
for _, comment := range params.Comments {
44
-
@Comment(CommentProps{Comment: comment})
45
-
}
0
0
0
0
0
0
46
</div>
47
</div>
48
}
···
1
package partials
2
3
+
import "fmt"
4
+
5
templ Discussion(params DiscussionProps) {
6
<div class="card">
7
<div class="flex items-center gap-2">
···
11
<form
12
hx-post="/comment/new"
13
hx-swap="afterbegin"
14
+
hx-target="#comment-feed"
15
hx-disabled-elt="#post-comment-button"
16
@htmx:after-request="text = ''"
17
x-data="{ text: '' }"
···
31
<div class="text-sm text-text-muted">
32
<span x-text="text.length"></span> / 256
33
</div>
34
+
<button type="submit" id="post-comment-button" class="btn btn-primary w-fit">
0
0
0
0
35
Post Comment
36
</button>
37
</div>
38
</div>
39
</form>
40
+
<div
41
+
id="comment-feed"
42
+
hx-trigger="load"
43
+
hx-swap="innerHTML"
44
+
hx-get={ templ.SafeURL(fmt.Sprintf("/%s/session/%s/feed", params.StudySessionDid, params.StudySessionRkey)) }
45
+
class="flex flex-col gap-4 mt-2"
46
+
>
47
+
<div class="flex justify-center py-4">
48
+
<i data-lucide="loader-circle" class="w-6 h-6 animate-spin text-text-muted"></i>
49
+
</div>
50
</div>
51
</div>
52
}
+1
-1
internal/server/views/partials/edit-comment.templ
···
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"
···
8
<form
9
hx-post={ templ.SafeURL("/comment/edit/" + params.Comment.Rkey) }
10
hx-target={ "#" + elementId }
11
+
hx-swap="outerHTML"
12
hx-disabled-elt="#update-comment-button,#cancel-comment-button"
13
x-data="{ text: '' }"
14
x-init="text = $el.querySelector('textarea').value"
+13
-2
internal/server/views/partials/partials.go
···
220
}
221
222
type DiscussionProps struct {
223
-
Comments []db.CommentFeedItem
224
-
StudySessionUri string
0
225
}
226
227
type CommentProps struct {
228
Comment db.CommentFeedItem
0
229
}
230
231
type EditCommentProps struct {
232
Comment db.Comment
233
}
0
0
0
0
0
0
0
0
0
···
220
}
221
222
type DiscussionProps struct {
223
+
StudySessionUri string
224
+
StudySessionDid string
225
+
StudySessionRkey string
226
}
227
228
type CommentProps struct {
229
Comment db.CommentFeedItem
230
+
DoesOwn bool
231
}
232
233
type EditCommentProps struct {
234
Comment db.Comment
235
}
236
+
237
+
type CommentFeedProps struct {
238
+
// The current logged in user
239
+
User *types.User
240
+
Feed []db.CommentFeedItem
241
+
NextPage int
242
+
StudySessionDid string
243
+
StudySessionRkey string
244
+
}
+1
-1
internal/server/views/partials/reactions.templ
···
40
}
41
</div>
42
}
43
-
<div class="inline-block text-left w-fit">
44
<button @click="open = !open" id="reaction-button" type="button" class="btn rounded-full hover:bg-bg py-1 px-2">
45
<i class="w-5 h-5" data-lucide="smile-plus"></i>
46
</button>
···
40
}
41
</div>
42
}
43
+
<div class="inline-block text-left w-fit" title="reactions">
44
<button @click="open = !open" id="reaction-button" type="button" class="btn rounded-full hover:bg-bg py-1 px-2">
45
<i class="w-5 h-5" data-lucide="smile-plus"></i>
46
</button>
+1
-1
internal/server/views/partials/study-session.templ
···
156
SessionRkey: params.StudySession.Rkey,
157
ReactionEvents: params.StudySession.Reactions,
158
})
159
-
<a href={ studySessionUrl }>
160
<i class="w-5 h-5" data-lucide="message-square-share"></i>
161
</a>
162
</div>
···
156
SessionRkey: params.StudySession.Rkey,
157
ReactionEvents: params.StudySession.Reactions,
158
})
159
+
<a href={ studySessionUrl } title="comments">
160
<i class="w-5 h-5" data-lucide="message-square-share"></i>
161
</a>
162
</div>
+3
-1
internal/server/views/study-session.templ
···
15
StudySession: params.StudySession,
16
})
17
@partials.Discussion(partials.DiscussionProps{
18
-
StudySessionUri: params.StudySession.StudySessionAt().String(),
0
0
19
})
20
</div>
21
}
···
15
StudySession: params.StudySession,
16
})
17
@partials.Discussion(partials.DiscussionProps{
18
+
StudySessionDid: params.StudySession.Did,
19
+
StudySessionRkey: params.StudySession.Rkey,
20
+
StudySessionUri: params.StudySession.StudySessionAt().String(),
21
})
22
</div>
23
}