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