Monorepo for Tangled

appview: replace `IssueComment` to `Comment`

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me 0cdf333a 345fbd28

verified
+98 -386
+6 -186
appview/db/issues.go
··· 100 100 } 101 101 102 102 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 103 - issueMap := make(map[string]*models.Issue) // at-uri -> issue 103 + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 104 104 105 105 var conditions []string 106 106 var args []any ··· 196 196 } 197 197 } 198 198 199 - atUri := issue.AtUri().String() 200 - issueMap[atUri] = &issue 199 + issueMap[issue.AtUri()] = &issue 201 200 } 202 201 203 202 // collect reverse repos ··· 229 228 // collect comments 230 229 issueAts := slices.Collect(maps.Keys(issueMap)) 231 230 232 - comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) 231 + comments, err := GetComments(e, orm.FilterIn("subject_at", issueAts)) 233 232 if err != nil { 234 233 return nil, fmt.Errorf("failed to query comments: %w", err) 235 234 } 236 235 for i := range comments { 237 - issueAt := comments[i].IssueAt 236 + issueAt := comments[i].Subject 238 237 if issue, ok := issueMap[issueAt]; ok { 239 238 issue.Comments = append(issue.Comments, comments[i]) 240 239 } ··· 246 245 return nil, fmt.Errorf("failed to query labels: %w", err) 247 246 } 248 247 for issueAt, labels := range allLabels { 249 - if issue, ok := issueMap[issueAt.String()]; ok { 248 + if issue, ok := issueMap[issueAt]; ok { 250 249 issue.Labels = labels 251 250 } 252 251 } ··· 257 256 return nil, fmt.Errorf("failed to query reference_links: %w", err) 258 257 } 259 258 for issueAt, references := range allReferencs { 260 - if issue, ok := issueMap[issueAt.String()]; ok { 259 + if issue, ok := issueMap[issueAt]; ok { 261 260 issue.References = references 262 261 } 263 262 } ··· 293 292 294 293 func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) { 295 294 return GetIssuesPaginated(e, pagination.Page{}, filters...) 296 - } 297 - 298 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 299 - result, err := tx.Exec( 300 - `insert into issue_comments ( 301 - did, 302 - rkey, 303 - issue_at, 304 - body, 305 - reply_to, 306 - created, 307 - edited 308 - ) 309 - values (?, ?, ?, ?, ?, ?, null) 310 - on conflict(did, rkey) do update set 311 - issue_at = excluded.issue_at, 312 - body = excluded.body, 313 - edited = case 314 - when 315 - issue_comments.issue_at != excluded.issue_at 316 - or issue_comments.body != excluded.body 317 - or issue_comments.reply_to != excluded.reply_to 318 - then ? 319 - else issue_comments.edited 320 - end`, 321 - c.Did, 322 - c.Rkey, 323 - c.IssueAt, 324 - c.Body, 325 - c.ReplyTo, 326 - c.Created.Format(time.RFC3339), 327 - time.Now().Format(time.RFC3339), 328 - ) 329 - if err != nil { 330 - return 0, err 331 - } 332 - 333 - id, err := result.LastInsertId() 334 - if err != nil { 335 - return 0, err 336 - } 337 - 338 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 339 - return 0, fmt.Errorf("put reference_links: %w", err) 340 - } 341 - 342 - return id, nil 343 - } 344 - 345 - func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 346 - var conditions []string 347 - var args []any 348 - for _, filter := range filters { 349 - conditions = append(conditions, filter.Condition()) 350 - args = append(args, filter.Arg()...) 351 - } 352 - 353 - whereClause := "" 354 - if conditions != nil { 355 - whereClause = " where " + strings.Join(conditions, " and ") 356 - } 357 - 358 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 359 - 360 - _, err := e.Exec(query, args...) 361 - return err 362 - } 363 - 364 - func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 365 - commentMap := make(map[string]*models.IssueComment) 366 - 367 - var conditions []string 368 - var args []any 369 - for _, filter := range filters { 370 - conditions = append(conditions, filter.Condition()) 371 - args = append(args, filter.Arg()...) 372 - } 373 - 374 - whereClause := "" 375 - if conditions != nil { 376 - whereClause = " where " + strings.Join(conditions, " and ") 377 - } 378 - 379 - query := fmt.Sprintf(` 380 - select 381 - id, 382 - did, 383 - rkey, 384 - issue_at, 385 - reply_to, 386 - body, 387 - created, 388 - edited, 389 - deleted 390 - from 391 - issue_comments 392 - %s 393 - `, whereClause) 394 - 395 - rows, err := e.Query(query, args...) 396 - if err != nil { 397 - return nil, err 398 - } 399 - defer rows.Close() 400 - 401 - for rows.Next() { 402 - var comment models.IssueComment 403 - var created string 404 - var rkey, edited, deleted, replyTo sql.Null[string] 405 - err := rows.Scan( 406 - &comment.Id, 407 - &comment.Did, 408 - &rkey, 409 - &comment.IssueAt, 410 - &replyTo, 411 - &comment.Body, 412 - &created, 413 - &edited, 414 - &deleted, 415 - ) 416 - if err != nil { 417 - return nil, err 418 - } 419 - 420 - // this is a remnant from old times, newer comments always have rkey 421 - if rkey.Valid { 422 - comment.Rkey = rkey.V 423 - } 424 - 425 - if t, err := time.Parse(time.RFC3339, created); err == nil { 426 - comment.Created = t 427 - } 428 - 429 - if edited.Valid { 430 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 431 - comment.Edited = &t 432 - } 433 - } 434 - 435 - if deleted.Valid { 436 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 437 - comment.Deleted = &t 438 - } 439 - } 440 - 441 - if replyTo.Valid { 442 - comment.ReplyTo = &replyTo.V 443 - } 444 - 445 - atUri := comment.AtUri().String() 446 - commentMap[atUri] = &comment 447 - } 448 - 449 - if err = rows.Err(); err != nil { 450 - return nil, err 451 - } 452 - 453 - // collect references for each comments 454 - commentAts := slices.Collect(maps.Keys(commentMap)) 455 - allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 456 - if err != nil { 457 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 458 - } 459 - for commentAt, references := range allReferencs { 460 - if comment, ok := commentMap[commentAt.String()]; ok { 461 - comment.References = references 462 - } 463 - } 464 - 465 - var comments []models.IssueComment 466 - for _, c := range commentMap { 467 - comments = append(comments, *c) 468 - } 469 - 470 - sort.Slice(comments, func(i, j int) bool { 471 - return comments[i].Created.After(comments[j].Created) 472 - }) 473 - 474 - return comments, nil 475 295 } 476 296 477 297 func DeleteIssues(tx *sql.Tx, did, rkey string) error {
+13 -24
appview/db/reference.go
··· 11 11 "tangled.org/core/orm" 12 12 ) 13 13 14 - // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 14 + // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 15 15 // It will ignore missing refLinks. 16 16 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 17 17 var ( ··· 53 53 values %s 54 54 ) 55 55 select 56 - i.did, i.rkey, 57 - c.did, c.rkey 56 + i.at_uri, c.at_uri 58 57 from input inp 59 58 join repos r 60 59 on r.did = inp.owner_did ··· 62 61 join issues i 63 62 on i.repo_at = r.at_uri 64 63 and i.issue_id = inp.issue_id 65 - left join issue_comments c 64 + left join comments c 66 65 on inp.comment_id is not null 67 - and c.issue_at = i.at_uri 66 + and c.subject_at = i.at_uri 68 67 and c.id = inp.comment_id 69 68 `, 70 69 strings.Join(vals, ","), ··· 79 78 80 79 for rows.Next() { 81 80 // Scan rows 82 - var issueOwner, issueRkey string 83 - var commentOwner, commentRkey sql.NullString 81 + var issueUri string 82 + var commentUri sql.NullString 84 83 var uri syntax.ATURI 85 - if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 84 + if err := rows.Scan(&issueUri, &commentUri); err != nil { 86 85 return nil, err 87 86 } 88 - if commentOwner.Valid && commentRkey.Valid { 89 - uri = syntax.ATURI(fmt.Sprintf( 90 - "at://%s/%s/%s", 91 - commentOwner.String, 92 - tangled.RepoIssueCommentNSID, 93 - commentRkey.String, 94 - )) 87 + if commentUri.Valid { 88 + uri = syntax.ATURI(commentUri.String) 95 89 } else { 96 - uri = syntax.ATURI(fmt.Sprintf( 97 - "at://%s/%s/%s", 98 - issueOwner, 99 - tangled.RepoIssueNSID, 100 - issueRkey, 101 - )) 90 + uri = syntax.ATURI(issueUri) 102 91 } 103 92 uris = append(uris, uri) 104 93 } ··· 282 271 return nil, fmt.Errorf("get issue backlinks: %w", err) 283 272 } 284 273 backlinks = append(backlinks, ls...) 285 - ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 274 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 286 275 if err != nil { 287 276 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 288 277 } ··· 351 340 rows, err := e.Query( 352 341 fmt.Sprintf( 353 342 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 354 - from issue_comments c 343 + from comments c 355 344 join issues i 356 - on i.at_uri = c.issue_at 345 + on i.at_uri = c.subject_at 357 346 join repos r 358 347 on r.at_uri = i.repo_at 359 348 where %s`,
+15 -6
appview/ingester.go
··· 891 891 } 892 892 893 893 switch e.Commit.Operation { 894 - case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 894 + case jmodels.CommitOperationUpdate: 895 895 raw := json.RawMessage(e.Commit.Record) 896 896 record := tangled.RepoIssueComment{} 897 897 err = json.Unmarshal(raw, &record) ··· 899 899 return fmt.Errorf("invalid record: %w", err) 900 900 } 901 901 902 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 902 + // convert 'sh.tangled.repo.issue.comment' to 'sh.tangled.comment' 903 + comment, err := models.CommentFromRecord(syntax.DID(did), syntax.RecordKey(rkey), tangled.Comment{ 904 + Body: record.Body, 905 + CreatedAt: record.CreatedAt, 906 + Mentions: record.Mentions, 907 + References: record.References, 908 + ReplyTo: record.ReplyTo, 909 + Subject: record.Issue, 910 + }) 903 911 if err != nil { 904 912 return fmt.Errorf("failed to parse comment from record: %w", err) 905 913 } 906 914 907 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 915 + if err := comment.Validate(); err != nil { 908 916 return fmt.Errorf("failed to validate comment: %w", err) 909 917 } 910 918 ··· 914 922 } 915 923 defer tx.Rollback() 916 924 917 - _, err = db.AddIssueComment(tx, *comment) 925 + err = db.PutComment(tx, comment) 918 926 if err != nil { 919 - return fmt.Errorf("failed to create issue comment: %w", err) 927 + return fmt.Errorf("failed to create comment: %w", err) 920 928 } 921 929 922 930 return tx.Commit() 923 931 924 932 case jmodels.CommitOperationDelete: 925 - if err := db.DeleteIssueComments( 933 + if err := db.DeleteComments( 926 934 ddb, 927 935 orm.FilterEq("did", did), 936 + orm.FilterEq("collection", e.Commit.Collection), 928 937 orm.FilterEq("rkey", rkey), 929 938 ); err != nil { 930 939 return fmt.Errorf("failed to delete issue comment record: %w", err)
+38 -36
appview/issues/issues.go
··· 402 402 403 403 body := r.FormValue("body") 404 404 if body == "" { 405 - rp.pages.Notice(w, "issue", "Body is required") 405 + rp.pages.Notice(w, "issue-comment", "Body is required") 406 406 return 407 407 } 408 408 409 - replyToUri := r.FormValue("reply-to") 410 - var replyTo *string 411 - if replyToUri != "" { 412 - replyTo = &replyToUri 409 + var replyTo *syntax.ATURI 410 + replyToRaw := r.FormValue("reply-to") 411 + if replyToRaw != "" { 412 + aturi, err := syntax.ParseATURI(replyToRaw) 413 + if err != nil { 414 + rp.pages.Notice(w, "issue-comment", "reply-to should be valid AT-URI") 415 + return 416 + } 417 + replyTo = &aturi 413 418 } 414 419 415 420 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 416 421 417 - comment := models.IssueComment{ 418 - Did: user.Active.Did, 422 + comment := models.Comment{ 423 + Did: syntax.DID(user.Active.Did), 424 + Collection: tangled.CommentNSID, 419 425 Rkey: tid.TID(), 420 - IssueAt: issue.AtUri().String(), 426 + Subject: issue.AtUri(), 421 427 ReplyTo: replyTo, 422 428 Body: body, 423 429 Created: time.Now(), 424 430 Mentions: mentions, 425 431 References: references, 426 432 } 427 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 433 + if err = comment.Validate(); err != nil { 428 434 l.Error("failed to validate comment", "err", err) 429 435 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 430 436 return 431 437 } 432 - record := comment.AsRecord() 433 438 434 439 client, err := rp.oauth.AuthorizedClient(r) 435 440 if err != nil { ··· 440 445 441 446 // create a record first 442 447 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 443 - Collection: tangled.RepoIssueCommentNSID, 444 - Repo: comment.Did, 448 + Collection: comment.Collection.String(), 449 + Repo: comment.Did.String(), 445 450 Rkey: comment.Rkey, 446 451 Record: &lexutil.LexiconTypeDecoder{ 447 - Val: &record, 452 + Val: comment.AsRecord(), 448 453 }, 449 454 }) 450 455 if err != nil { ··· 467 472 } 468 473 defer tx.Rollback() 469 474 470 - commentId, err := db.AddIssueComment(tx, comment) 475 + err = db.PutComment(tx, &comment) 471 476 if err != nil { 472 477 l.Error("failed to create comment", "err", err) 473 478 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") ··· 483 488 // reset atUri to make rollback a no-op 484 489 atUri = "" 485 490 486 - // notify about the new comment 487 - comment.Id = commentId 488 - 489 491 rp.notifier.NewIssueComment(r.Context(), &comment, mentions) 490 492 491 493 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 492 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId)) 494 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 493 495 } 494 496 495 497 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { ··· 504 506 } 505 507 506 508 commentId := chi.URLParam(r, "commentId") 507 - comments, err := db.GetIssueComments( 509 + comments, err := db.GetComments( 508 510 rp.db, 509 511 orm.FilterEq("id", commentId), 510 512 ) ··· 540 542 } 541 543 542 544 commentId := chi.URLParam(r, "commentId") 543 - comments, err := db.GetIssueComments( 545 + comments, err := db.GetComments( 544 546 rp.db, 545 547 orm.FilterEq("id", commentId), 546 548 ) ··· 556 558 } 557 559 comment := comments[0] 558 560 559 - if comment.Did != user.Active.Did { 561 + if comment.Did.String() != user.Active.Did { 560 562 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 561 563 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 562 564 return ··· 586 588 newComment.Edited = &now 587 589 newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 588 590 589 - record := newComment.AsRecord() 590 - 591 591 tx, err := rp.db.Begin() 592 592 if err != nil { 593 593 l.Error("failed to start transaction", "err", err) ··· 596 596 } 597 597 defer tx.Rollback() 598 598 599 - _, err = db.AddIssueComment(tx, newComment) 599 + err = db.PutComment(tx, &newComment) 600 600 if err != nil { 601 601 l.Error("failed to perferom update-description query", "err", err) 602 602 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 606 606 607 607 // rkey is optional, it was introduced later 608 608 if newComment.Rkey != "" { 609 + // TODO: update correct comment 610 + 609 611 // update the record on pds 610 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey) 612 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey) 611 613 if err != nil { 612 614 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 613 - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 615 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update comment, no record found on PDS.") 614 616 return 615 617 } 616 618 617 619 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 618 - Collection: tangled.RepoIssueCommentNSID, 619 - Repo: user.Active.Did, 620 + Collection: newComment.Collection.String(), 621 + Repo: newComment.Did.String(), 620 622 Rkey: newComment.Rkey, 621 623 SwapRecord: ex.Cid, 622 624 Record: &lexutil.LexiconTypeDecoder{ 623 - Val: &record, 625 + Val: newComment.AsRecord(), 624 626 }, 625 627 }) 626 628 if err != nil { ··· 650 652 } 651 653 652 654 commentId := chi.URLParam(r, "commentId") 653 - comments, err := db.GetIssueComments( 655 + comments, err := db.GetComments( 654 656 rp.db, 655 657 orm.FilterEq("id", commentId), 656 658 ) ··· 686 688 } 687 689 688 690 commentId := chi.URLParam(r, "commentId") 689 - comments, err := db.GetIssueComments( 691 + comments, err := db.GetComments( 690 692 rp.db, 691 693 orm.FilterEq("id", commentId), 692 694 ) ··· 722 724 } 723 725 724 726 commentId := chi.URLParam(r, "commentId") 725 - comments, err := db.GetIssueComments( 727 + comments, err := db.GetComments( 726 728 rp.db, 727 729 orm.FilterEq("id", commentId), 728 730 ) ··· 738 740 } 739 741 comment := comments[0] 740 742 741 - if comment.Did != user.Active.Did { 743 + if comment.Did.String() != user.Active.Did { 742 744 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 743 745 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 744 746 return ··· 751 753 752 754 // optimistic deletion 753 755 deleted := time.Now() 754 - err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 756 + err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 755 757 if err != nil { 756 758 l.Error("failed to delete comment", "err", err) 757 759 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 767 769 return 768 770 } 769 771 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 770 - Collection: tangled.RepoIssueCommentNSID, 771 - Repo: user.Active.Did, 772 + Collection: comment.Collection.String(), 773 + Repo: comment.Did.String(), 772 774 Rkey: comment.Rkey, 773 775 }) 774 776 if err != nil {
+8 -89
appview/models/issue.go
··· 26 26 27 27 // optionally, populate this when querying for reverse mappings 28 28 // like comment counts, parent repo etc. 29 - Comments []IssueComment 29 + Comments []Comment 30 30 Labels LabelState 31 31 Repo *Repo 32 32 } ··· 62 62 } 63 63 64 64 type CommentListItem struct { 65 - Self *IssueComment 66 - Replies []*IssueComment 65 + Self *Comment 66 + Replies []*Comment 67 67 } 68 68 69 69 func (it *CommentListItem) Participants() []syntax.DID { ··· 88 88 89 89 func (i *Issue) CommentList() []CommentListItem { 90 90 // Create a map to quickly find comments by their aturi 91 - toplevel := make(map[string]*CommentListItem) 92 - var replies []*IssueComment 91 + toplevel := make(map[syntax.ATURI]*CommentListItem) 92 + var replies []*Comment 93 93 94 94 // collect top level comments into the map 95 95 for _, comment := range i.Comments { 96 96 if comment.IsTopLevel() { 97 - toplevel[comment.AtUri().String()] = &CommentListItem{ 97 + toplevel[comment.AtUri()] = &CommentListItem{ 98 98 Self: &comment, 99 99 } 100 100 } else { ··· 115 115 } 116 116 117 117 // sort everything 118 - sortFunc := func(a, b *IssueComment) bool { 118 + sortFunc := func(a, b *Comment) bool { 119 119 return a.Created.Before(b.Created) 120 120 } 121 121 sort.Slice(listing, func(i, j int) bool { ··· 144 144 addParticipant(i.Did) 145 145 146 146 for _, c := range i.Comments { 147 - addParticipant(c.Did) 147 + addParticipant(c.Did.String()) 148 148 } 149 149 150 150 return participants ··· 171 171 Open: true, // new issues are open by default 172 172 } 173 173 } 174 - 175 - type IssueComment struct { 176 - Id int64 177 - Did string 178 - Rkey string 179 - IssueAt string 180 - ReplyTo *string 181 - Body string 182 - Created time.Time 183 - Edited *time.Time 184 - Deleted *time.Time 185 - Mentions []syntax.DID 186 - References []syntax.ATURI 187 - } 188 - 189 - func (i *IssueComment) AtUri() syntax.ATURI { 190 - return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.Did, tangled.RepoIssueCommentNSID, i.Rkey)) 191 - } 192 - 193 - func (i *IssueComment) AsRecord() tangled.RepoIssueComment { 194 - mentions := make([]string, len(i.Mentions)) 195 - for i, did := range i.Mentions { 196 - mentions[i] = string(did) 197 - } 198 - references := make([]string, len(i.References)) 199 - for i, uri := range i.References { 200 - references[i] = string(uri) 201 - } 202 - return tangled.RepoIssueComment{ 203 - Body: i.Body, 204 - Issue: i.IssueAt, 205 - CreatedAt: i.Created.Format(time.RFC3339), 206 - ReplyTo: i.ReplyTo, 207 - Mentions: mentions, 208 - References: references, 209 - } 210 - } 211 - 212 - func (i *IssueComment) IsTopLevel() bool { 213 - return i.ReplyTo == nil 214 - } 215 - 216 - func (i *IssueComment) IsReply() bool { 217 - return i.ReplyTo != nil 218 - } 219 - 220 - func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) { 221 - created, err := time.Parse(time.RFC3339, record.CreatedAt) 222 - if err != nil { 223 - created = time.Now() 224 - } 225 - 226 - ownerDid := did 227 - 228 - if _, err = syntax.ParseATURI(record.Issue); err != nil { 229 - return nil, err 230 - } 231 - 232 - i := record 233 - mentions := make([]syntax.DID, len(record.Mentions)) 234 - for i, did := range record.Mentions { 235 - mentions[i] = syntax.DID(did) 236 - } 237 - references := make([]syntax.ATURI, len(record.References)) 238 - for i, uri := range i.References { 239 - references[i] = syntax.ATURI(uri) 240 - } 241 - 242 - comment := IssueComment{ 243 - Did: ownerDid, 244 - Rkey: rkey, 245 - Body: record.Body, 246 - IssueAt: record.Issue, 247 - ReplyTo: record.ReplyTo, 248 - Created: created, 249 - Mentions: mentions, 250 - References: references, 251 - } 252 - 253 - return &comment, nil 254 - }
+4 -4
appview/notify/db/db.go
··· 122 122 ) 123 123 } 124 124 125 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 126 - issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 125 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 126 + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.Subject)) 127 127 if err != nil { 128 128 log.Printf("NewIssueComment: failed to get issues: %v", err) 129 129 return 130 130 } 131 131 if len(issues) == 0 { 132 - log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 132 + log.Printf("NewIssueComment: no issue found for %s", comment.Subject) 133 133 return 134 134 } 135 135 issue := issues[0] ··· 147 147 148 148 // find the parent thread, and add all DIDs from here to the recipient list 149 149 for _, t := range issue.CommentList() { 150 - if t.Self.AtUri().String() == parentAtUri { 150 + if t.Self.AtUri() == parentAtUri { 151 151 for _, p := range t.Participants() { 152 152 recipients.Insert(p) 153 153 }
+1 -1
appview/notify/merged_notifier.go
··· 57 57 m.fanout("NewIssue", ctx, issue, mentions) 58 58 } 59 59 60 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 60 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 61 61 m.fanout("NewIssueComment", ctx, comment, mentions) 62 62 } 63 63
+2 -2
appview/notify/notifier.go
··· 14 14 DeleteStar(ctx context.Context, star *models.Star) 15 15 16 16 NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) 17 - NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) 17 + NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) 18 18 NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) 19 19 DeleteIssue(ctx context.Context, issue *models.Issue) 20 20 ··· 43 43 func (m *BaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {} 44 44 45 45 func (m *BaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {} 46 - func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 46 + func (m *BaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 47 47 } 48 48 func (m *BaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {} 49 49 func (m *BaseNotifier) DeleteIssue(ctx context.Context, issue *models.Issue) {}
+3 -3
appview/notify/posthog/notifier.go
··· 179 179 } 180 180 } 181 181 182 - func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 182 + func (n *posthogNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 183 183 err := n.client.Enqueue(posthog.Capture{ 184 - DistinctId: comment.Did, 184 + DistinctId: comment.Did.String(), 185 185 Event: "new_issue_comment", 186 186 Properties: posthog.Properties{ 187 - "issue_at": comment.IssueAt, 187 + "issue_at": comment.Subject, 188 188 "mentions": mentions, 189 189 }, 190 190 })
+4 -4
appview/pages/pages.go
··· 1004 1004 LoggedInUser *oauth.MultiAccountUser 1005 1005 RepoInfo repoinfo.RepoInfo 1006 1006 Issue *models.Issue 1007 - Comment *models.IssueComment 1007 + Comment *models.Comment 1008 1008 } 1009 1009 1010 1010 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1015 1015 LoggedInUser *oauth.MultiAccountUser 1016 1016 RepoInfo repoinfo.RepoInfo 1017 1017 Issue *models.Issue 1018 - Comment *models.IssueComment 1018 + Comment *models.Comment 1019 1019 } 1020 1020 1021 1021 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1026 1026 LoggedInUser *oauth.MultiAccountUser 1027 1027 RepoInfo repoinfo.RepoInfo 1028 1028 Issue *models.Issue 1029 - Comment *models.IssueComment 1029 + Comment *models.Comment 1030 1030 } 1031 1031 1032 1032 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1037 1037 LoggedInUser *oauth.MultiAccountUser 1038 1038 RepoInfo repoinfo.RepoInfo 1039 1039 Issue *models.Issue 1040 - Comment *models.IssueComment 1040 + Comment *models.Comment 1041 1041 } 1042 1042 1043 1043 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+2 -2
appview/pages/templates/repo/issues/fragments/commentList.html
··· 41 41 {{ define "topLevelComment" }} 42 42 <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 43 <div class="flex-shrink-0"> 44 - {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} 44 + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 45 45 </div> 46 46 <div class="flex-1 min-w-0"> 47 47 {{ template "repo/issues/fragments/issueCommentHeader" . }} ··· 53 53 {{ define "replyComment" }} 54 54 <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 55 55 <div class="flex-shrink-0"> 56 - {{ template "user/fragments/picLink" (list .Comment.Did "size-8 mr-1") }} 56 + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 57 57 </div> 58 58 <div class="flex-1 min-w-0"> 59 59 {{ template "repo/issues/fragments/issueCommentHeader" . }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ $handle := resolve .Comment.Did }} 3 + {{ $handle := resolve .Comment.Did.String }} 4 4 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 5 {{ template "hats" $ }} 6 6 <span class="before:content-['·']"></span> 7 7 {{ template "timestamp" . }} 8 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 8 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 9 9 {{ if and $isCommentOwner (not .Comment.Deleted) }} 10 10 {{ template "editIssueComment" . }} 11 11 {{ template "deleteIssueComment" . }}
-27
appview/validator/issue.go
··· 4 4 "fmt" 5 5 "strings" 6 6 7 - "tangled.org/core/appview/db" 8 7 "tangled.org/core/appview/models" 9 - "tangled.org/core/orm" 10 8 ) 11 - 12 - func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 13 - // if comments have parents, only ingest ones that are 1 level deep 14 - if comment.ReplyTo != nil { 15 - parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 16 - if err != nil { 17 - return fmt.Errorf("failed to fetch parent comment: %w", err) 18 - } 19 - if len(parents) != 1 { 20 - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 21 - } 22 - 23 - // depth check 24 - parent := parents[0] 25 - if parent.ReplyTo != nil { 26 - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 27 - } 28 - } 29 - 30 - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 31 - return fmt.Errorf("body is empty after HTML sanitization") 32 - } 33 - 34 - return nil 35 - } 36 9 37 10 func (v *Validator) ValidateIssue(issue *models.Issue) error { 38 11 if issue.Title == "" {