+8
appview/db/db.go
+8
appview/db/db.go
···
248
248
return nil
249
249
})
250
250
251
+
runMigration(db, "add-rkey-to-comments", func(tx *sql.Tx) error {
252
+
_, err := tx.Exec(`
253
+
alter table comments drop column comment_at;
254
+
alter table comments add column rkey text;
255
+
`)
256
+
return err
257
+
})
258
+
251
259
runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
252
260
// add unconstrained column
253
261
_, err := tx.Exec(`
+52
-9
appview/db/issues.go
+52
-9
appview/db/issues.go
···
27
27
type Comment struct {
28
28
OwnerDid string
29
29
RepoAt syntax.ATURI
30
-
CommentAt syntax.ATURI
30
+
Rkey string
31
31
Issue int
32
32
CommentId int
33
33
Body string
···
201
201
return &issue, comments, nil
202
202
}
203
203
204
-
func NewComment(e Execer, comment *Comment) error {
205
-
query := `insert into comments (owner_did, repo_at, comment_at, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
204
+
func NewIssueComment(e Execer, comment *Comment) error {
205
+
query := `insert into comments (owner_did, repo_at, rkey, issue_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
206
206
_, err := e.Exec(
207
207
query,
208
208
comment.OwnerDid,
209
209
comment.RepoAt,
210
-
comment.CommentAt,
210
+
comment.Rkey,
211
211
comment.Issue,
212
212
comment.CommentId,
213
213
comment.Body,
···
218
218
func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) {
219
219
var comments []Comment
220
220
221
-
rows, err := e.Query(`select owner_did, issue_id, comment_id, comment_at, body, created from comments where repo_at = ? and issue_id = ? order by created asc`, repoAt, issueId)
221
+
rows, err := e.Query(`
222
+
select
223
+
owner_did,
224
+
issue_id,
225
+
comment_id,
226
+
rkey,
227
+
body,
228
+
created,
229
+
edited,
230
+
deleted
231
+
from
232
+
comments
233
+
where
234
+
repo_at = ? and issue_id = ?
235
+
order by
236
+
created asc`,
237
+
repoAt,
238
+
issueId,
239
+
)
222
240
if err == sql.ErrNoRows {
223
241
return []Comment{}, nil
224
242
}
···
230
248
for rows.Next() {
231
249
var comment Comment
232
250
var createdAt string
233
-
err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt)
251
+
var deletedAt, editedAt, rkey sql.NullString
252
+
err := rows.Scan(&comment.OwnerDid, &comment.Issue, &comment.CommentId, &rkey, &comment.Body, &createdAt, &editedAt, &deletedAt)
234
253
if err != nil {
235
254
return nil, err
236
255
}
···
241
260
}
242
261
comment.Created = &createdAtTime
243
262
263
+
if deletedAt.Valid {
264
+
deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
265
+
if err != nil {
266
+
return nil, err
267
+
}
268
+
comment.Deleted = &deletedTime
269
+
}
270
+
271
+
if editedAt.Valid {
272
+
editedTime, err := time.Parse(time.RFC3339, editedAt.String)
273
+
if err != nil {
274
+
return nil, err
275
+
}
276
+
comment.Edited = &editedTime
277
+
}
278
+
279
+
if rkey.Valid {
280
+
comment.Rkey = rkey.String
281
+
}
282
+
244
283
comments = append(comments, comment)
245
284
}
246
285
···
254
293
func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) {
255
294
query := `
256
295
select
257
-
owner_did, body, comment_at, created, deleted, edited
296
+
owner_did, body, rkey, created, deleted, edited
258
297
from
259
298
comments where repo_at = ? and issue_id = ? and comment_id = ?
260
299
`
···
262
301
263
302
var comment Comment
264
303
var createdAt string
265
-
var deletedAt, editedAt sql.NullString
266
-
err := row.Scan(&comment.OwnerDid, &comment.Body, &comment.CommentAt, &createdAt, &deletedAt, &editedAt)
304
+
var deletedAt, editedAt, rkey sql.NullString
305
+
err := row.Scan(&comment.OwnerDid, &comment.Body, &rkey, &createdAt, &deletedAt, &editedAt)
267
306
if err != nil {
268
307
return nil, err
269
308
}
···
288
327
return nil, err
289
328
}
290
329
comment.Edited = &editedTime
330
+
}
331
+
332
+
if rkey.Valid {
333
+
comment.Rkey = rkey.String
291
334
}
292
335
293
336
comment.RepoAt = repoAt
+15
-8
appview/pages/templates/fragments/issueComment.html
+15
-8
appview/pages/templates/fragments/issueComment.html
···
9
9
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
10
10
{{ if $isIssueAuthor }}
11
11
<span class="before:content-['·']"></span>
12
-
<span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center">
12
+
<span class="rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
13
13
author
14
14
</span>
15
15
{{ end }}
···
19
19
href="#{{ .CommentId }}"
20
20
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
21
21
id="{{ .CommentId }}">
22
-
{{ .Created | timeFmt }}
22
+
{{ if .Deleted }}
23
+
deleted {{ .Deleted | timeFmt }}
24
+
{{ else if .Edited }}
25
+
edited {{ .Edited | timeFmt }}
26
+
{{ else }}
27
+
{{ .Created | timeFmt }}
28
+
{{ end }}
23
29
</a>
24
30
25
31
{{ $isCommentOwner := eq $.LoggedInUser.Did .OwnerDid }}
···
32
38
>
33
39
{{ i "pencil" "w-4 h-4" }}
34
40
</button>
35
-
<button class="btn px-2 py-1 text-sm text-red-500" hx-delete="">
41
+
<button
42
+
class="btn px-2 py-1 text-sm text-red-500"
43
+
hx-delete="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
44
+
hx-swap="outerHTML"
45
+
hx-target="#comment-container-{{.CommentId}}"
46
+
>
36
47
{{ i "trash-2" "w-4 h-4" }}
37
48
</button>
38
49
{{ end }}
39
50
40
-
{{ if .Deleted }}
41
-
<span class="before:content-['·']">deleted {{ .Deleted | timeFmt }}</span>
42
-
{{ end }}
43
-
44
51
</div>
45
52
{{ if not .Deleted }}
46
-
<div class="prose">
53
+
<div class="prose dark:prose-invert">
47
54
{{ .Body | markdown }}
48
55
</div>
49
56
{{ end }}
+74
-35
appview/state/repo.go
+74
-35
appview/state/repo.go
···
933
933
}
934
934
935
935
commentId := rand.IntN(1000000)
936
+
rkey := s.TID()
936
937
937
-
err := db.NewComment(s.db, &db.Comment{
938
+
err := db.NewIssueComment(s.db, &db.Comment{
938
939
OwnerDid: user.Did,
939
940
RepoAt: f.RepoAt,
940
941
Issue: issueIdInt,
941
942
CommentId: commentId,
942
943
Body: body,
944
+
Rkey: rkey,
943
945
})
944
946
if err != nil {
945
947
log.Println("failed to create comment", err)
···
962
964
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
963
965
Collection: tangled.RepoIssueCommentNSID,
964
966
Repo: user.Did,
965
-
Rkey: s.TID(),
967
+
Rkey: rkey,
966
968
Record: &lexutil.LexiconTypeDecoder{
967
969
Val: &tangled.RepoIssueComment{
968
970
Repo: &atUri,
···
1098
1100
// extract form value
1099
1101
newBody := r.FormValue("body")
1100
1102
client, _ := s.auth.AuthorizedClient(r)
1101
-
log.Println("comment at", comment.CommentAt)
1102
-
rkey := comment.CommentAt.RecordKey()
1103
+
rkey := comment.Rkey
1103
1104
1104
1105
// optimistic update
1106
+
edited := time.Now()
1105
1107
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
1106
1108
if err != nil {
1107
1109
log.Println("failed to perferom update-description query", err)
···
1109
1111
return
1110
1112
}
1111
1113
1112
-
// update the record on pds
1113
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey.String())
1114
-
if err != nil {
1115
-
// failed to get record
1116
-
log.Println(err, rkey.String())
1117
-
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
1118
-
return
1119
-
}
1120
-
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
1121
-
record, _ := data.UnmarshalJSON(value)
1114
+
// rkey is optional, it was introduced later
1115
+
if comment.Rkey != "" {
1116
+
// update the record on pds
1117
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
1118
+
if err != nil {
1119
+
// failed to get record
1120
+
log.Println(err, rkey)
1121
+
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
1122
+
return
1123
+
}
1124
+
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
1125
+
record, _ := data.UnmarshalJSON(value)
1122
1126
1123
-
repoAt := record["repo"].(string)
1124
-
issueAt := record["issue"].(string)
1125
-
createdAt := record["createdAt"].(string)
1126
-
commentIdInt64 := int64(commentIdInt)
1127
+
repoAt := record["repo"].(string)
1128
+
issueAt := record["issue"].(string)
1129
+
createdAt := record["createdAt"].(string)
1130
+
commentIdInt64 := int64(commentIdInt)
1127
1131
1128
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1129
-
Collection: tangled.RepoNSID,
1130
-
Repo: user.Did,
1131
-
Rkey: rkey.String(),
1132
-
SwapRecord: ex.Cid,
1133
-
Record: &lexutil.LexiconTypeDecoder{
1134
-
Val: &tangled.RepoIssueComment{
1135
-
Repo: &repoAt,
1136
-
Issue: issueAt,
1137
-
CommentId: &commentIdInt64,
1138
-
Owner: &comment.OwnerDid,
1139
-
Body: &newBody,
1140
-
CreatedAt: &createdAt,
1132
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
1133
+
Collection: tangled.RepoIssueCommentNSID,
1134
+
Repo: user.Did,
1135
+
Rkey: rkey,
1136
+
SwapRecord: ex.Cid,
1137
+
Record: &lexutil.LexiconTypeDecoder{
1138
+
Val: &tangled.RepoIssueComment{
1139
+
Repo: &repoAt,
1140
+
Issue: issueAt,
1141
+
CommentId: &commentIdInt64,
1142
+
Owner: &comment.OwnerDid,
1143
+
Body: &newBody,
1144
+
CreatedAt: &createdAt,
1145
+
},
1141
1146
},
1142
-
},
1143
-
})
1144
-
if err != nil {
1145
-
log.Println(err)
1147
+
})
1148
+
if err != nil {
1149
+
log.Println(err)
1150
+
}
1146
1151
}
1147
1152
1148
1153
// optimistic update for htmx
···
1150
1155
user.Did: user.Handle,
1151
1156
}
1152
1157
comment.Body = newBody
1158
+
comment.Edited = &edited
1153
1159
1154
1160
// return new comment body with htmx
1155
1161
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
···
1181
1187
return
1182
1188
}
1183
1189
1190
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1191
+
if err != nil {
1192
+
log.Println("failed to get issue", err)
1193
+
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1194
+
return
1195
+
}
1196
+
1184
1197
commentId := chi.URLParam(r, "comment_id")
1185
1198
commentIdInt, err := strconv.Atoi(commentId)
1186
1199
if err != nil {
···
1206
1219
}
1207
1220
1208
1221
// optimistic deletion
1222
+
deleted := time.Now()
1209
1223
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
1210
1224
if err != nil {
1211
1225
log.Println("failed to delete comment")
···
1214
1228
}
1215
1229
1216
1230
// delete from pds
1231
+
if comment.Rkey != "" {
1232
+
client, _ := s.auth.AuthorizedClient(r)
1233
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
1234
+
Collection: tangled.GraphFollowNSID,
1235
+
Repo: user.Did,
1236
+
Rkey: comment.Rkey,
1237
+
})
1238
+
if err != nil {
1239
+
log.Println(err)
1240
+
}
1241
+
}
1242
+
1243
+
// optimistic update for htmx
1244
+
didHandleMap := map[string]string{
1245
+
user.Did: user.Handle,
1246
+
}
1247
+
comment.Body = ""
1248
+
comment.Deleted = &deleted
1217
1249
1218
1250
// htmx fragment of comment after deletion
1251
+
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
1252
+
LoggedInUser: user,
1253
+
RepoInfo: f.RepoInfo(s, user),
1254
+
DidHandleMap: didHandleMap,
1255
+
Issue: issue,
1256
+
Comment: comment,
1257
+
})
1219
1258
return
1220
1259
}
1221
1260