+9
appview/db/db.go
+9
appview/db/db.go
···
248
248
return nil
249
249
})
250
250
251
+
runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
252
+
// add unconstrained column
253
+
_, err := tx.Exec(`
254
+
alter table comments add column deleted text; -- timestamp
255
+
alter table comments add column edited text; -- timestamp
256
+
`)
257
+
return err
258
+
})
259
+
251
260
return &DB{db}, nil
252
261
}
253
262
+71
-1
appview/db/issues.go
+71
-1
appview/db/issues.go
···
27
27
type Comment struct {
28
28
OwnerDid string
29
29
RepoAt syntax.ATURI
30
-
CommentAt string
30
+
CommentAt syntax.ATURI
31
31
Issue int
32
32
CommentId int
33
33
Body string
34
34
Created *time.Time
35
+
Deleted *time.Time
36
+
Edited *time.Time
35
37
}
36
38
37
39
func NewIssue(tx *sql.Tx, issue *Issue) error {
···
247
249
}
248
250
249
251
return comments, nil
252
+
}
253
+
254
+
func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) {
255
+
query := `
256
+
select
257
+
owner_did, body, comment_at, created, deleted, edited
258
+
from
259
+
comments where repo_at = ? and issue_id = ? and comment_id = ?
260
+
`
261
+
row := e.QueryRow(query, repoAt, issueId, commentId)
262
+
263
+
var comment Comment
264
+
var createdAt string
265
+
var deletedAt, editedAt sql.NullString
266
+
err := row.Scan(&comment.OwnerDid, &comment.Body, &comment.CommentAt, &createdAt, &deletedAt, &editedAt)
267
+
if err != nil {
268
+
return nil, err
269
+
}
270
+
271
+
createdTime, err := time.Parse(time.RFC3339, createdAt)
272
+
if err != nil {
273
+
return nil, err
274
+
}
275
+
comment.Created = &createdTime
276
+
277
+
if deletedAt.Valid {
278
+
deletedTime, err := time.Parse(time.RFC3339, deletedAt.String)
279
+
if err != nil {
280
+
return nil, err
281
+
}
282
+
comment.Deleted = &deletedTime
283
+
}
284
+
285
+
if editedAt.Valid {
286
+
editedTime, err := time.Parse(time.RFC3339, editedAt.String)
287
+
if err != nil {
288
+
return nil, err
289
+
}
290
+
comment.Edited = &editedTime
291
+
}
292
+
293
+
comment.RepoAt = repoAt
294
+
comment.Issue = issueId
295
+
comment.CommentId = commentId
296
+
297
+
return &comment, nil
298
+
}
299
+
300
+
func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error {
301
+
_, err := e.Exec(
302
+
`
303
+
update comments
304
+
set body = ?,
305
+
edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
306
+
where repo_at = ? and issue_id = ? and comment_id = ?
307
+
`, newBody, repoAt, issueId, commentId)
308
+
return err
309
+
}
310
+
311
+
func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error {
312
+
_, err := e.Exec(
313
+
`
314
+
update comments
315
+
set body = "",
316
+
deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
317
+
where repo_at = ? and issue_id = ? and comment_id = ?
318
+
`, repoAt, issueId, commentId)
319
+
return err
250
320
}
251
321
252
322
func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
+23
appview/pages/pages.go
+23
appview/pages/pages.go
···
534
534
return p.executeRepo("repo/issues/new", w, params)
535
535
}
536
536
537
+
type EditIssueCommentParams struct {
538
+
LoggedInUser *auth.User
539
+
RepoInfo RepoInfo
540
+
Issue *db.Issue
541
+
Comment *db.Comment
542
+
}
543
+
544
+
func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error {
545
+
return p.executePlain("fragments/editIssueComment", w, params)
546
+
}
547
+
548
+
type SingleIssueCommentParams struct {
549
+
LoggedInUser *auth.User
550
+
DidHandleMap map[string]string
551
+
RepoInfo RepoInfo
552
+
Issue *db.Issue
553
+
Comment *db.Comment
554
+
}
555
+
556
+
func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error {
557
+
return p.executePlain("fragments/issueComment", w, params)
558
+
}
559
+
537
560
type RepoNewPullParams struct {
538
561
LoggedInUser *auth.User
539
562
RepoInfo RepoInfo
+23
-18
appview/pages/templates/fragments/diff.html
+23
-18
appview/pages/templates/fragments/diff.html
···
79
79
This is a binary file and will not be displayed.
80
80
</p>
81
81
{{ else }}
82
-
<pre class="overflow-auto">
83
-
{{- range .TextFragments -}}
84
-
<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{ .Header }}</div>
85
-
{{- range .Lines -}}
86
-
{{- if eq .Op.String "+" -}}
87
-
<div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 p-1 w-full min-w-fit"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div>
88
-
{{- end -}}
89
-
90
-
{{- if eq .Op.String "-" -}}
91
-
<div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 p-1 w-full min-w-fit"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div>
92
-
{{- end -}}
93
-
94
-
{{- if eq .Op.String " " -}}
95
-
<div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 px"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div>
96
-
{{- end -}}
97
-
98
-
{{- end -}}
99
-
{{- end -}}
82
+
<pre class="overflow-x-auto">
83
+
{{- range .TextFragments -}}
84
+
<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{- .Header -}}</div><div class="overflow-x-auto"><div class="min-w-full inline-block">
85
+
{{- range .Lines -}}
86
+
{{- if eq .Op.String "+" -}}
87
+
<div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full">
88
+
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
89
+
<div class="p-1 whitespace-pre">{{ .Line }}</div>
90
+
</div>
91
+
{{- end -}}
92
+
{{- if eq .Op.String "-" -}}
93
+
<div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full">
94
+
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
95
+
<div class="p-1 whitespace-pre">{{ .Line }}</div>
96
+
</div>
97
+
{{- end -}}
98
+
{{- if eq .Op.String " " -}}
99
+
<div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full">
100
+
<div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div>
101
+
<div class="p-1 whitespace-pre">{{ .Line }}</div>
102
+
</div>
103
+
{{- end -}}
104
+
{{- end -}}</div></div>{{- end -}}
100
105
</pre>
101
106
{{- end -}}
102
107
{{ end }}
+52
appview/pages/templates/fragments/editIssueComment.html
+52
appview/pages/templates/fragments/editIssueComment.html
···
1
+
{{ define "fragments/editIssueComment" }}
2
+
{{ with .Comment }}
3
+
<div id="comment-container-{{.CommentId}}">
4
+
<div class="flex items-center gap-2 mb-2 text-gray-500 text-sm">
5
+
{{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }}
6
+
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
7
+
8
+
<!-- show user "hats" -->
9
+
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
10
+
{{ if $isIssueAuthor }}
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">
13
+
author
14
+
</span>
15
+
{{ end }}
16
+
17
+
<span class="before:content-['·']"></span>
18
+
<a
19
+
href="#{{ .CommentId }}"
20
+
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
21
+
id="{{ .CommentId }}">
22
+
{{ .Created | timeFmt }}
23
+
</a>
24
+
25
+
<button
26
+
class="btn px-2 py-1 flex items-center gap-2 text-sm"
27
+
hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
28
+
hx-include="#edit-textarea-{{ .CommentId }}"
29
+
hx-target="#comment-container-{{ .CommentId }}"
30
+
hx-swap="outerHTML">
31
+
{{ i "check" "w-4 h-4" }}
32
+
</button>
33
+
<button
34
+
class="btn px-2 py-1 flex items-center gap-2 text-sm"
35
+
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/"
36
+
hx-target="#comment-container-{{ .CommentId }}"
37
+
hx-swap="outerHTML">
38
+
{{ i "x" "w-4 h-4" }}
39
+
</button>
40
+
<span id="comment-{{.CommentId}}-status"></span>
41
+
</div>
42
+
43
+
<div>
44
+
<textarea
45
+
id="edit-textarea-{{ .CommentId }}"
46
+
name="body"
47
+
class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea>
48
+
</div>
49
+
</div>
50
+
{{ end }}
51
+
{{ end }}
52
+
+52
appview/pages/templates/fragments/issueComment.html
+52
appview/pages/templates/fragments/issueComment.html
···
1
+
{{ define "fragments/issueComment" }}
2
+
{{ with .Comment }}
3
+
<div id="comment-container-{{.CommentId}}">
4
+
<div class="flex items-center gap-2 mb-2 text-gray-500 text-sm">
5
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
6
+
<a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a>
7
+
8
+
<!-- show user "hats" -->
9
+
{{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }}
10
+
{{ if $isIssueAuthor }}
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">
13
+
author
14
+
</span>
15
+
{{ end }}
16
+
17
+
<span class="before:content-['·']"></span>
18
+
<a
19
+
href="#{{ .CommentId }}"
20
+
class="text-gray-500 hover:text-gray-500 hover:underline no-underline"
21
+
id="{{ .CommentId }}">
22
+
{{ .Created | timeFmt }}
23
+
</a>
24
+
25
+
{{ $isCommentOwner := eq $.LoggedInUser.Did .OwnerDid }}
26
+
{{ if and $isCommentOwner (not .Deleted) }}
27
+
<button
28
+
class="btn px-2 py-1 text-sm"
29
+
hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit"
30
+
hx-swap="outerHTML"
31
+
hx-target="#comment-container-{{.CommentId}}"
32
+
>
33
+
{{ i "pencil" "w-4 h-4" }}
34
+
</button>
35
+
<button class="btn px-2 py-1 text-sm text-red-500" hx-delete="">
36
+
{{ i "trash-2" "w-4 h-4" }}
37
+
</button>
38
+
{{ end }}
39
+
40
+
{{ if .Deleted }}
41
+
<span class="before:content-['·']">deleted {{ .Deleted | timeFmt }}</span>
42
+
{{ end }}
43
+
44
+
</div>
45
+
{{ if not .Deleted }}
46
+
<div class="prose">
47
+
{{ .Body | markdown }}
48
+
</div>
49
+
{{ end }}
50
+
</div>
51
+
{{ end }}
52
+
{{ end }}
+3
-25
appview/pages/templates/repo/issues/issue.html
+3
-25
appview/pages/templates/repo/issues/issue.html
···
1
-
{{ define "title" }}{{ .Issue.Title }} · issue #{{ .Issue.IssueId }} ·{{ .RepoInfo.FullName }}{{ end }}
1
+
{{ define "title" }}{{ .Issue.Title }} · issue #{{ .Issue.IssueId }} · {{ .RepoInfo.FullName }}{{ end }}
2
2
3
3
{{ define "repoContent" }}
4
4
<header class="pb-4">
···
49
49
{{ range $index, $comment := .Comments }}
50
50
<div
51
51
id="comment-{{ .CommentId }}"
52
-
class="rounded bg-white px-6 py-4 relative dark:bg-gray-800"
53
-
>
52
+
class="rounded bg-white px-6 py-4 relative dark:bg-gray-800">
54
53
{{ if eq $index 0 }}
55
54
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div>
56
55
{{ else }}
57
56
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-700" ></div>
58
57
{{ end }}
59
-
<div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400">
60
-
{{ $owner := index $.DidHandleMap .OwnerDid }}
61
-
<span class="text-sm">
62
-
<a
63
-
href="/{{ $owner }}"
64
-
class="no-underline hover:underline"
65
-
>{{ $owner }}</a
66
-
>
67
-
</span>
68
58
69
-
<span class="before:content-['·']"></span>
70
-
<a
71
-
href="#{{ .CommentId }}"
72
-
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800"
73
-
id="{{ .CommentId }}"
74
-
title="{{ .Created | longTimeFmt }}"
75
-
>
76
-
{{ .Created | timeFmt }}
77
-
</a>
78
-
</div>
79
-
<div class="prose dark:prose-invert">
80
-
{{ .Body | markdown }}
81
-
</div>
59
+
{{ template "fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}}
82
60
</div>
83
61
{{ end }}
84
62
</section>
+236
-1
appview/state/repo.go
+236
-1
appview/state/repo.go
···
14
14
"strings"
15
15
"time"
16
16
17
+
"github.com/bluesky-social/indigo/atproto/data"
17
18
"github.com/bluesky-social/indigo/atproto/identity"
18
19
"github.com/bluesky-social/indigo/atproto/syntax"
19
20
securejoin "github.com/cyphar/filepath-securejoin"
···
907
908
}
908
909
}
909
910
910
-
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
911
+
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
911
912
user := s.auth.GetUser(r)
912
913
f, err := fullyResolvedRepo(r)
913
914
if err != nil {
···
982
983
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
983
984
return
984
985
}
986
+
}
987
+
988
+
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
989
+
user := s.auth.GetUser(r)
990
+
f, err := fullyResolvedRepo(r)
991
+
if err != nil {
992
+
log.Println("failed to get repo and knot", err)
993
+
return
994
+
}
995
+
996
+
issueId := chi.URLParam(r, "issue")
997
+
issueIdInt, err := strconv.Atoi(issueId)
998
+
if err != nil {
999
+
http.Error(w, "bad issue id", http.StatusBadRequest)
1000
+
log.Println("failed to parse issue id", err)
1001
+
return
1002
+
}
1003
+
1004
+
commentId := chi.URLParam(r, "comment_id")
1005
+
commentIdInt, err := strconv.Atoi(commentId)
1006
+
if err != nil {
1007
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1008
+
log.Println("failed to parse issue id", err)
1009
+
return
1010
+
}
1011
+
1012
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1013
+
if err != nil {
1014
+
log.Println("failed to get issue", err)
1015
+
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1016
+
return
1017
+
}
1018
+
1019
+
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
1020
+
if err != nil {
1021
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1022
+
return
1023
+
}
1024
+
1025
+
identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid)
1026
+
if err != nil {
1027
+
log.Println("failed to resolve did")
1028
+
return
1029
+
}
1030
+
1031
+
didHandleMap := make(map[string]string)
1032
+
if !identity.Handle.IsInvalidHandle() {
1033
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
1034
+
} else {
1035
+
didHandleMap[identity.DID.String()] = identity.DID.String()
1036
+
}
1037
+
1038
+
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
1039
+
LoggedInUser: user,
1040
+
RepoInfo: f.RepoInfo(s, user),
1041
+
DidHandleMap: didHandleMap,
1042
+
Issue: issue,
1043
+
Comment: comment,
1044
+
})
1045
+
}
1046
+
1047
+
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
1048
+
user := s.auth.GetUser(r)
1049
+
f, err := fullyResolvedRepo(r)
1050
+
if err != nil {
1051
+
log.Println("failed to get repo and knot", err)
1052
+
return
1053
+
}
1054
+
1055
+
issueId := chi.URLParam(r, "issue")
1056
+
issueIdInt, err := strconv.Atoi(issueId)
1057
+
if err != nil {
1058
+
http.Error(w, "bad issue id", http.StatusBadRequest)
1059
+
log.Println("failed to parse issue id", err)
1060
+
return
1061
+
}
1062
+
1063
+
commentId := chi.URLParam(r, "comment_id")
1064
+
commentIdInt, err := strconv.Atoi(commentId)
1065
+
if err != nil {
1066
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1067
+
log.Println("failed to parse issue id", err)
1068
+
return
1069
+
}
1070
+
1071
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
1072
+
if err != nil {
1073
+
log.Println("failed to get issue", err)
1074
+
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
1075
+
return
1076
+
}
1077
+
1078
+
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
1079
+
if err != nil {
1080
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1081
+
return
1082
+
}
1083
+
1084
+
if comment.OwnerDid != user.Did {
1085
+
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
1086
+
return
1087
+
}
1088
+
1089
+
switch r.Method {
1090
+
case http.MethodGet:
1091
+
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
1092
+
LoggedInUser: user,
1093
+
RepoInfo: f.RepoInfo(s, user),
1094
+
Issue: issue,
1095
+
Comment: comment,
1096
+
})
1097
+
case http.MethodPost:
1098
+
// extract form value
1099
+
newBody := r.FormValue("body")
1100
+
client, _ := s.auth.AuthorizedClient(r)
1101
+
log.Println("comment at", comment.CommentAt)
1102
+
rkey := comment.CommentAt.RecordKey()
1103
+
1104
+
// optimistic update
1105
+
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
1106
+
if err != nil {
1107
+
log.Println("failed to perferom update-description query", err)
1108
+
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
1109
+
return
1110
+
}
1111
+
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)
1122
+
1123
+
repoAt := record["repo"].(string)
1124
+
issueAt := record["issue"].(string)
1125
+
createdAt := record["createdAt"].(string)
1126
+
commentIdInt64 := int64(commentIdInt)
1127
+
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,
1141
+
},
1142
+
},
1143
+
})
1144
+
if err != nil {
1145
+
log.Println(err)
1146
+
}
1147
+
1148
+
// optimistic update for htmx
1149
+
didHandleMap := map[string]string{
1150
+
user.Did: user.Handle,
1151
+
}
1152
+
comment.Body = newBody
1153
+
1154
+
// return new comment body with htmx
1155
+
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
1156
+
LoggedInUser: user,
1157
+
RepoInfo: f.RepoInfo(s, user),
1158
+
DidHandleMap: didHandleMap,
1159
+
Issue: issue,
1160
+
Comment: comment,
1161
+
})
1162
+
return
1163
+
1164
+
}
1165
+
1166
+
}
1167
+
1168
+
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
1169
+
user := s.auth.GetUser(r)
1170
+
f, err := fullyResolvedRepo(r)
1171
+
if err != nil {
1172
+
log.Println("failed to get repo and knot", err)
1173
+
return
1174
+
}
1175
+
1176
+
issueId := chi.URLParam(r, "issue")
1177
+
issueIdInt, err := strconv.Atoi(issueId)
1178
+
if err != nil {
1179
+
http.Error(w, "bad issue id", http.StatusBadRequest)
1180
+
log.Println("failed to parse issue id", err)
1181
+
return
1182
+
}
1183
+
1184
+
commentId := chi.URLParam(r, "comment_id")
1185
+
commentIdInt, err := strconv.Atoi(commentId)
1186
+
if err != nil {
1187
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1188
+
log.Println("failed to parse issue id", err)
1189
+
return
1190
+
}
1191
+
1192
+
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
1193
+
if err != nil {
1194
+
http.Error(w, "bad comment id", http.StatusBadRequest)
1195
+
return
1196
+
}
1197
+
1198
+
if comment.OwnerDid != user.Did {
1199
+
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
1200
+
return
1201
+
}
1202
+
1203
+
if comment.Deleted != nil {
1204
+
http.Error(w, "comment already deleted", http.StatusBadRequest)
1205
+
return
1206
+
}
1207
+
1208
+
// optimistic deletion
1209
+
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
1210
+
if err != nil {
1211
+
log.Println("failed to delete comment")
1212
+
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
1213
+
return
1214
+
}
1215
+
1216
+
// delete from pds
1217
+
1218
+
// htmx fragment of comment after deletion
1219
+
return
985
1220
}
986
1221
987
1222
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
+7
-1
appview/state/router.go
+7
-1
appview/state/router.go
···
73
73
r.Use(AuthMiddleware(s))
74
74
r.Get("/new", s.NewIssue)
75
75
r.Post("/new", s.NewIssue)
76
-
r.Post("/{issue}/comment", s.IssueComment)
76
+
r.Post("/{issue}/comment", s.NewIssueComment)
77
+
r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) {
78
+
r.Get("/", s.IssueComment)
79
+
r.Delete("/", s.DeleteIssueComment)
80
+
r.Get("/edit", s.EditIssueComment)
81
+
r.Post("/edit", s.EditIssueComment)
82
+
})
77
83
r.Post("/{issue}/close", s.CloseIssue)
78
84
r.Post("/{issue}/reopen", s.ReopenIssue)
79
85
})