Monorepo for Tangled tangled.org

add rkeys to comments

this allows to actually edit and delete them from pds etc.

Changed files
+149 -52
appview
db
pages
templates
fragments
state
+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
··· 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
··· 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
··· 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