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