appview: replace IssueComment to Comment #866

open
opened by boltless.me targeting master from sl/wnrvrwyvrlzo
Changed files
+93 -380
appview
db
issues
models
notify
pages
templates
repo
issues
state
validator
+6 -185
appview/db/issues.go
··· 99 99 } 100 100 101 101 func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 102 - issueMap := make(map[string]*models.Issue) // at-uri -> issue 102 + issueMap := make(map[syntax.ATURI]*models.Issue) // at-uri -> issue 103 103 104 104 var conditions []string 105 105 var args []any ··· 195 195 } 196 196 } 197 197 198 - atUri := issue.AtUri().String() 199 - issueMap[atUri] = &issue 198 + issueMap[issue.AtUri()] = &issue 200 199 } 201 200 202 201 // collect reverse repos ··· 228 227 // collect comments 229 228 issueAts := slices.Collect(maps.Keys(issueMap)) 230 229 231 - comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts)) 230 + comments, err := GetComments(e, FilterIn("subject_at", issueAts)) 232 231 if err != nil { 233 232 return nil, fmt.Errorf("failed to query comments: %w", err) 234 233 } 235 234 for i := range comments { 236 - issueAt := comments[i].IssueAt 235 + issueAt := comments[i].Subject 237 236 if issue, ok := issueMap[issueAt]; ok { 238 237 issue.Comments = append(issue.Comments, comments[i]) 239 238 } ··· 245 244 return nil, fmt.Errorf("failed to query labels: %w", err) 246 245 } 247 246 for issueAt, labels := range allLabels { 248 - if issue, ok := issueMap[issueAt.String()]; ok { 247 + if issue, ok := issueMap[issueAt]; ok { 249 248 issue.Labels = labels 250 249 } 251 250 } ··· 256 255 return nil, fmt.Errorf("failed to query reference_links: %w", err) 257 256 } 258 257 for issueAt, references := range allReferencs { 259 - if issue, ok := issueMap[issueAt.String()]; ok { 258 + if issue, ok := issueMap[issueAt]; ok { 260 259 issue.References = references 261 260 } 262 261 } ··· 350 349 return ids, nil 351 350 } 352 351 353 - func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) { 354 - result, err := tx.Exec( 355 - `insert into issue_comments ( 356 - did, 357 - rkey, 358 - issue_at, 359 - body, 360 - reply_to, 361 - created, 362 - edited 363 - ) 364 - values (?, ?, ?, ?, ?, ?, null) 365 - on conflict(did, rkey) do update set 366 - issue_at = excluded.issue_at, 367 - body = excluded.body, 368 - edited = case 369 - when 370 - issue_comments.issue_at != excluded.issue_at 371 - or issue_comments.body != excluded.body 372 - or issue_comments.reply_to != excluded.reply_to 373 - then ? 374 - else issue_comments.edited 375 - end`, 376 - c.Did, 377 - c.Rkey, 378 - c.IssueAt, 379 - c.Body, 380 - c.ReplyTo, 381 - c.Created.Format(time.RFC3339), 382 - time.Now().Format(time.RFC3339), 383 - ) 384 - if err != nil { 385 - return 0, err 386 - } 387 - 388 - id, err := result.LastInsertId() 389 - if err != nil { 390 - return 0, err 391 - } 392 - 393 - if err := putReferences(tx, c.AtUri(), c.References); err != nil { 394 - return 0, fmt.Errorf("put reference_links: %w", err) 395 - } 396 - 397 - return id, nil 398 - } 399 - 400 - func DeleteIssueComments(e Execer, filters ...filter) error { 401 - var conditions []string 402 - var args []any 403 - for _, filter := range filters { 404 - conditions = append(conditions, filter.Condition()) 405 - args = append(args, filter.Arg()...) 406 - } 407 - 408 - whereClause := "" 409 - if conditions != nil { 410 - whereClause = " where " + strings.Join(conditions, " and ") 411 - } 412 - 413 - query := fmt.Sprintf(`update issue_comments set body = "", deleted = strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ', 'now') %s`, whereClause) 414 - 415 - _, err := e.Exec(query, args...) 416 - return err 417 - } 418 - 419 - func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) { 420 - commentMap := make(map[string]*models.IssueComment) 421 - 422 - var conditions []string 423 - var args []any 424 - for _, filter := range filters { 425 - conditions = append(conditions, filter.Condition()) 426 - args = append(args, filter.Arg()...) 427 - } 428 - 429 - whereClause := "" 430 - if conditions != nil { 431 - whereClause = " where " + strings.Join(conditions, " and ") 432 - } 433 - 434 - query := fmt.Sprintf(` 435 - select 436 - id, 437 - did, 438 - rkey, 439 - issue_at, 440 - reply_to, 441 - body, 442 - created, 443 - edited, 444 - deleted 445 - from 446 - issue_comments 447 - %s 448 - `, whereClause) 449 - 450 - rows, err := e.Query(query, args...) 451 - if err != nil { 452 - return nil, err 453 - } 454 - 455 - for rows.Next() { 456 - var comment models.IssueComment 457 - var created string 458 - var rkey, edited, deleted, replyTo sql.Null[string] 459 - err := rows.Scan( 460 - &comment.Id, 461 - &comment.Did, 462 - &rkey, 463 - &comment.IssueAt, 464 - &replyTo, 465 - &comment.Body, 466 - &created, 467 - &edited, 468 - &deleted, 469 - ) 470 - if err != nil { 471 - return nil, err 472 - } 473 - 474 - // this is a remnant from old times, newer comments always have rkey 475 - if rkey.Valid { 476 - comment.Rkey = rkey.V 477 - } 478 - 479 - if t, err := time.Parse(time.RFC3339, created); err == nil { 480 - comment.Created = t 481 - } 482 - 483 - if edited.Valid { 484 - if t, err := time.Parse(time.RFC3339, edited.V); err == nil { 485 - comment.Edited = &t 486 - } 487 - } 488 - 489 - if deleted.Valid { 490 - if t, err := time.Parse(time.RFC3339, deleted.V); err == nil { 491 - comment.Deleted = &t 492 - } 493 - } 494 - 495 - if replyTo.Valid { 496 - comment.ReplyTo = &replyTo.V 497 - } 498 - 499 - atUri := comment.AtUri().String() 500 - commentMap[atUri] = &comment 501 - } 502 - 503 - if err = rows.Err(); err != nil { 504 - return nil, err 505 - } 506 - 507 - // collect references for each comments 508 - commentAts := slices.Collect(maps.Keys(commentMap)) 509 - allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts)) 510 - if err != nil { 511 - return nil, fmt.Errorf("failed to query reference_links: %w", err) 512 - } 513 - for commentAt, references := range allReferencs { 514 - if comment, ok := commentMap[commentAt.String()]; ok { 515 - comment.References = references 516 - } 517 - } 518 - 519 - var comments []models.IssueComment 520 - for _, c := range commentMap { 521 - comments = append(comments, *c) 522 - } 523 - 524 - sort.Slice(comments, func(i, j int) bool { 525 - return comments[i].Created.After(comments[j].Created) 526 - }) 527 - 528 - return comments, nil 529 - } 530 - 531 352 func DeleteIssues(tx *sql.Tx, did, rkey string) error { 532 353 _, err := tx.Exec( 533 354 `delete from issues
+13 -24
appview/db/reference.go
··· 10 10 "tangled.org/core/appview/models" 11 11 ) 12 12 13 - // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. 13 + // ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs. 14 14 // It will ignore missing refLinks. 15 15 func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) { 16 16 var ( ··· 52 52 values %s 53 53 ) 54 54 select 55 - i.did, i.rkey, 56 - c.did, c.rkey 55 + i.at_uri, c.at_uri 57 56 from input inp 58 57 join repos r 59 58 on r.did = inp.owner_did ··· 61 60 join issues i 62 61 on i.repo_at = r.at_uri 63 62 and i.issue_id = inp.issue_id 64 - left join issue_comments c 63 + left join comments c 65 64 on inp.comment_id is not null 66 - and c.issue_at = i.at_uri 65 + and c.subject_at = i.at_uri 67 66 and c.id = inp.comment_id 68 67 `, 69 68 strings.Join(vals, ","), ··· 78 77 79 78 for rows.Next() { 80 79 // Scan rows 81 - var issueOwner, issueRkey string 82 - var commentOwner, commentRkey sql.NullString 80 + var issueUri string 81 + var commentUri sql.NullString 83 82 var uri syntax.ATURI 84 - if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil { 83 + if err := rows.Scan(&issueUri, &commentUri); err != nil { 85 84 return nil, err 86 85 } 87 - if commentOwner.Valid && commentRkey.Valid { 88 - uri = syntax.ATURI(fmt.Sprintf( 89 - "at://%s/%s/%s", 90 - commentOwner.String, 91 - tangled.RepoIssueCommentNSID, 92 - commentRkey.String, 93 - )) 86 + if commentUri.Valid { 87 + uri = syntax.ATURI(commentUri.String) 94 88 } else { 95 - uri = syntax.ATURI(fmt.Sprintf( 96 - "at://%s/%s/%s", 97 - issueOwner, 98 - tangled.RepoIssueNSID, 99 - issueRkey, 100 - )) 89 + uri = syntax.ATURI(issueUri) 101 90 } 102 91 uris = append(uris, uri) 103 92 } ··· 281 270 return nil, fmt.Errorf("get issue backlinks: %w", err) 282 271 } 283 272 backlinks = append(backlinks, ls...) 284 - ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID]) 273 + ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID]) 285 274 if err != nil { 286 275 return nil, fmt.Errorf("get issue_comment backlinks: %w", err) 287 276 } ··· 350 339 rows, err := e.Query( 351 340 fmt.Sprintf( 352 341 `select r.did, r.name, i.issue_id, c.id, i.title, i.open 353 - from issue_comments c 342 + from comments c 354 343 join issues i 355 - on i.at_uri = c.issue_at 344 + on i.at_uri = c.subject_at 356 345 join repos r 357 346 on r.at_uri = i.repo_at 358 347 where %s`,
+19 -11
appview/ingester.go
··· 78 78 err = i.ingestString(e) 79 79 case tangled.RepoIssueNSID: 80 80 err = i.ingestIssue(ctx, e) 81 - case tangled.RepoIssueCommentNSID: 82 - err = i.ingestIssueComment(e) 81 + case tangled.CommentNSID: 82 + err = i.ingestComment(e) 83 83 case tangled.LabelDefinitionNSID: 84 84 err = i.ingestLabelDefinition(e) 85 85 case tangled.LabelOpNSID: ··· 867 867 return nil 868 868 } 869 869 870 - func (i *Ingester) ingestIssueComment(e *jmodels.Event) error { 870 + func (i *Ingester) ingestComment(e *jmodels.Event) error { 871 871 did := e.Did 872 872 rkey := e.Commit.RKey 873 873 874 874 var err error 875 875 876 - l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 876 + l := i.Logger.With("handler", "ingestComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 877 877 l.Info("ingesting record") 878 878 879 879 ddb, ok := i.Db.Execer.(*db.DB) ··· 884 884 switch e.Commit.Operation { 885 885 case jmodels.CommitOperationCreate, jmodels.CommitOperationUpdate: 886 886 raw := json.RawMessage(e.Commit.Record) 887 - record := tangled.RepoIssueComment{} 887 + record := tangled.Comment{} 888 888 err = json.Unmarshal(raw, &record) 889 889 if err != nil { 890 890 return fmt.Errorf("invalid record: %w", err) 891 891 } 892 892 893 - comment, err := models.IssueCommentFromRecord(did, rkey, record) 893 + comment, err := models.CommentFromRecord(did, rkey, record) 894 894 if err != nil { 895 895 return fmt.Errorf("failed to parse comment from record: %w", err) 896 896 } 897 897 898 - if err := i.Validator.ValidateIssueComment(comment); err != nil { 898 + // TODO: ingest pull comments 899 + // we aren't ingesting pull comments yet because pull itself isn't fully atprotated. 900 + // so we cannot know which round this comment is pointing to 901 + if comment.Subject.Collection().String() == tangled.RepoPullNSID { 902 + l.Info("skip ingesting pull comments") 903 + return nil 904 + } 905 + 906 + if err := comment.Validate(); err != nil { 899 907 return fmt.Errorf("failed to validate comment: %w", err) 900 908 } 901 909 ··· 905 913 } 906 914 defer tx.Rollback() 907 915 908 - _, err = db.AddIssueComment(tx, *comment) 916 + err = db.PutComment(tx, comment) 909 917 if err != nil { 910 - return fmt.Errorf("failed to create issue comment: %w", err) 918 + return fmt.Errorf("failed to create comment: %w", err) 911 919 } 912 920 913 921 return tx.Commit() 914 922 915 923 case jmodels.CommitOperationDelete: 916 - if err := db.DeleteIssueComments( 924 + if err := db.DeleteComments( 917 925 ddb, 918 926 db.FilterEq("did", did), 919 927 db.FilterEq("rkey", rkey), 920 928 ); err != nil { 921 - return fmt.Errorf("failed to delete issue comment record: %w", err) 929 + return fmt.Errorf("failed to delete comment record: %w", err) 922 930 } 923 931 924 932 return nil
+30 -28
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(r.FormValue("reply-to")) 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.refResolver.Resolve(r.Context(), body) 416 421 417 - comment := models.IssueComment{ 418 - Did: user.Did, 422 + comment := models.Comment{ 423 + Did: syntax.DID(user.Did), 419 424 Rkey: tid.TID(), 420 - IssueAt: issue.AtUri().String(), 425 + Subject: issue.AtUri(), 421 426 ReplyTo: replyTo, 422 427 Body: body, 423 428 Created: time.Now(), 424 429 Mentions: mentions, 425 430 References: references, 426 431 } 427 - if err = rp.validator.ValidateIssueComment(&comment); err != nil { 432 + if err = comment.Validate(); err != nil { 428 433 l.Error("failed to validate comment", "err", err) 429 434 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 430 435 return ··· 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: tangled.CommentNSID, 449 + Repo: user.Did, 445 450 Rkey: comment.Rkey, 446 451 Record: &lexutil.LexiconTypeDecoder{ 447 452 Val: &record, ··· 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 db.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 db.FilterEq("id", commentId), 546 548 ) ··· 556 558 } 557 559 comment := comments[0] 558 560 559 - if comment.Did != user.Did { 561 + if comment.Did.String() != user.Did { 560 562 l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 561 563 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 562 564 return ··· 596 598 } 597 599 defer tx.Rollback() 598 600 599 - _, err = db.AddIssueComment(tx, newComment) 601 + err = db.PutComment(tx, &newComment) 600 602 if err != nil { 601 603 l.Error("failed to perferom update-description query", "err", err) 602 604 rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") ··· 607 609 // rkey is optional, it was introduced later 608 610 if newComment.Rkey != "" { 609 611 // update the record on pds 610 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 612 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.CommentNSID, user.Did, comment.Rkey) 611 613 if err != nil { 612 614 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 613 615 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 615 617 } 616 618 617 619 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 618 - Collection: tangled.RepoIssueCommentNSID, 620 + Collection: tangled.CommentNSID, 619 621 Repo: user.Did, 620 622 Rkey: newComment.Rkey, 621 623 SwapRecord: ex.Cid, ··· 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 db.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 db.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 db.FilterEq("id", commentId), 728 730 ) ··· 738 740 } 739 741 comment := comments[0] 740 742 741 - if comment.Did != user.Did { 743 + if comment.Did.String() != user.Did { 742 744 l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.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, db.FilterEq("id", comment.Id)) 756 + err = db.DeleteComments(rp.db, db.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, 772 + Collection: tangled.CommentNSID, 771 773 Repo: user.Did, 772 774 Rkey: comment.Rkey, 773 775 })
+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
··· 118 118 ) 119 119 } 120 120 121 - func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 122 - issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt)) 121 + func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 122 + issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.Subject)) 123 123 if err != nil { 124 124 log.Printf("NewIssueComment: failed to get issues: %v", err) 125 125 return 126 126 } 127 127 if len(issues) == 0 { 128 - log.Printf("NewIssueComment: no issue found for %s", comment.IssueAt) 128 + log.Printf("NewIssueComment: no issue found for %s", comment.Subject) 129 129 return 130 130 } 131 131 issue := issues[0] ··· 140 140 141 141 // find the parent thread, and add all DIDs from here to the recipient list 142 142 for _, t := range allThreads { 143 - if t.Self.AtUri().String() == parentAtUri { 143 + if t.Self.AtUri() == parentAtUri { 144 144 recipients = append(recipients, t.Participants()...) 145 145 } 146 146 }
+1 -1
appview/notify/merged_notifier.go
··· 58 58 m.fanout("NewIssue", ctx, issue, mentions) 59 59 } 60 60 61 - func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) { 61 + func (m *mergedNotifier) NewIssueComment(ctx context.Context, comment *models.Comment, mentions []syntax.DID) { 62 62 m.fanout("NewIssueComment", ctx, comment, mentions) 63 63 } 64 64
+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
··· 989 989 LoggedInUser *oauth.User 990 990 RepoInfo repoinfo.RepoInfo 991 991 Issue *models.Issue 992 - Comment *models.IssueComment 992 + Comment *models.Comment 993 993 } 994 994 995 995 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { ··· 1000 1000 LoggedInUser *oauth.User 1001 1001 RepoInfo repoinfo.RepoInfo 1002 1002 Issue *models.Issue 1003 - Comment *models.IssueComment 1003 + Comment *models.Comment 1004 1004 } 1005 1005 1006 1006 func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { ··· 1011 1011 LoggedInUser *oauth.User 1012 1012 RepoInfo repoinfo.RepoInfo 1013 1013 Issue *models.Issue 1014 - Comment *models.IssueComment 1014 + Comment *models.Comment 1015 1015 } 1016 1016 1017 1017 func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { ··· 1022 1022 LoggedInUser *oauth.User 1023 1023 RepoInfo repoinfo.RepoInfo 1024 1024 Issue *models.Issue 1025 - Comment *models.IssueComment 1025 + Comment *models.Comment 1026 1026 } 1027 1027 1028 1028 func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error {
+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 - {{ template "user/fragments/picHandleLink" .Comment.Did }} 3 + {{ template "user/fragments/picHandleLink" .Comment.Did.String }} 4 4 {{ template "hats" $ }} 5 5 {{ template "timestamp" . }} 6 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 6 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 7 7 {{ if and $isCommentOwner (not .Comment.Deleted) }} 8 8 {{ template "editIssueComment" . }} 9 9 {{ template "deleteIssueComment" . }}
+1 -1
appview/state/state.go
··· 116 116 tangled.SpindleNSID, 117 117 tangled.StringNSID, 118 118 tangled.RepoIssueNSID, 119 - tangled.RepoIssueCommentNSID, 119 + tangled.CommentNSID, 120 120 tangled.LabelDefinitionNSID, 121 121 tangled.LabelOpNSID, 122 122 },
-26
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 8 ) 10 9 11 - func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 12 - // if comments have parents, only ingest ones that are 1 level deep 13 - if comment.ReplyTo != nil { 14 - parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) 15 - if err != nil { 16 - return fmt.Errorf("failed to fetch parent comment: %w", err) 17 - } 18 - if len(parents) != 1 { 19 - return fmt.Errorf("incorrect number of parent comments returned: %d", len(parents)) 20 - } 21 - 22 - // depth check 23 - parent := parents[0] 24 - if parent.ReplyTo != nil { 25 - return fmt.Errorf("incorrect depth, this comment is replying at depth >1") 26 - } 27 - } 28 - 29 - if sb := strings.TrimSpace(v.sanitizer.SanitizeDefault(comment.Body)); sb == "" { 30 - return fmt.Errorf("body is empty after HTML sanitization") 31 - } 32 - 33 - return nil 34 - } 35 - 36 10 func (v *Validator) ValidateIssue(issue *models.Issue) error { 37 11 if issue.Title == "" { 38 12 return fmt.Errorf("issue title is empty")