Monorepo for Tangled tangled.org

appview: unified comment fragments/handlers #1276

open opened by boltless.me targeting master from sl/comment

share as much handlers/fragments as possible. PR has still /.../comment endpoint to serve comment form htmx fragment. Due to how it is designed.

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

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mimbhgiqjb22
+645 -817
Diff #9
+11
appview/db/comments.go
··· 154 154 return err 155 155 } 156 156 157 + func GetComment(e Execer, filters ...orm.Filter) (models.Comment, error) { 158 + comments, err := GetComments(e, filters...) 159 + if err != nil { 160 + return models.Comment{}, err 161 + } 162 + if len(comments) != 1 { 163 + return models.Comment{}, fmt.Errorf("expected 1 comment, got %d", len(comments)) 164 + } 165 + return comments[0], nil 166 + } 167 + 157 168 func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { 158 169 var comments []models.Comment 159 170
+6 -1
appview/ingester.go
··· 1196 1196 return fmt.Errorf("failed to validate comment: %w", err) 1197 1197 } 1198 1198 1199 + var mentions []syntax.DID 1199 1200 var references []syntax.ATURI 1200 1201 if comment.Body.Original != nil { 1201 - _, references = i.MentionsResolver.Resolve(ctx, *comment.Body.Original) 1202 + mentions, references = i.MentionsResolver.Resolve(ctx, *comment.Body.Original) 1202 1203 } 1203 1204 1204 1205 tx, err := ddb.Begin() ··· 1216 1217 return err 1217 1218 } 1218 1219 1220 + if e.Commit.Operation == jmodels.CommitOperationCreate { 1221 + i.Notifier.NewComment(ctx, comment, mentions) 1222 + } 1223 + 1219 1224 case jmodels.CommitOperationDelete: 1220 1225 if err := db.DeleteComments( 1221 1226 ddb,
+4 -464
appview/issues/issues.go
··· 13 13 "github.com/bluesky-social/indigo/atproto/atclient" 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 17 - "github.com/go-chi/chi/v5" 18 16 19 17 "tangled.org/core/api/tangled" 20 18 "tangled.org/core/appview/config" ··· 133 131 defs[l.AtUri().String()] = &l 134 132 } 135 133 136 - rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 134 + err = rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 137 135 LoggedInUser: user, 138 136 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 139 137 Issue: issue, ··· 143 141 UserReacted: userReactions, 144 142 LabelDefs: defs, 145 143 }) 144 + if err != nil { 145 + l.Error("failed to render", "err", err) 146 + } 146 147 } 147 148 148 149 func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { ··· 389 390 } 390 391 } 391 392 392 - func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { 393 - l := rp.logger.With("handler", "NewIssueComment") 394 - user := rp.oauth.GetMultiAccountUser(r) 395 - f, err := rp.repoResolver.Resolve(r) 396 - if err != nil { 397 - l.Error("failed to get repo and knot", "err", err) 398 - return 399 - } 400 - 401 - issue, ok := r.Context().Value("issue").(*models.Issue) 402 - if !ok { 403 - l.Error("failed to get issue") 404 - rp.pages.Error404(w) 405 - return 406 - } 407 - 408 - body := r.FormValue("body") 409 - if body == "" { 410 - rp.pages.Notice(w, "issue-comment", "Body is required") 411 - return 412 - } 413 - 414 - // TODO(boltless): normalize markdown body 415 - normalizedBody := body 416 - _, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 - 418 - markdownBody := tangled.MarkupMarkdown{ 419 - Text: normalizedBody, 420 - Original: &body, 421 - Blobs: nil, 422 - } 423 - 424 - // ingest CID of issue record on-demand. 425 - // TODO(boltless): appview should ingest CID of atproto records 426 - cid, err := func() (syntax.CID, error) { 427 - ident, err := rp.idResolver.ResolveIdent(r.Context(), issue.Did) 428 - if err != nil { 429 - return "", err 430 - } 431 - 432 - xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} 433 - out, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoIssueNSID, issue.Did, issue.Rkey) 434 - if err != nil { 435 - return "", err 436 - } 437 - if out.Cid == nil { 438 - return "", fmt.Errorf("record CID is empty") 439 - } 440 - 441 - cid, err := syntax.ParseCID(*out.Cid) 442 - if err != nil { 443 - return "", err 444 - } 445 - 446 - return cid, nil 447 - }() 448 - if err != nil { 449 - rp.logger.Error("failed to backfill subject PR record", "err", err) 450 - rp.pages.Notice(w, "issue-comment", "failed to backfill subject record") 451 - return 452 - } 453 - issueStrongRef := comatproto.RepoStrongRef{ 454 - Uri: issue.AtUri().String(), 455 - Cid: cid.String(), 456 - } 457 - 458 - var replyTo *comatproto.RepoStrongRef 459 - replyToUriRaw := r.FormValue("reply-to-uri") 460 - replyToCidRaw := r.FormValue("reply-to-cid") 461 - if replyToUriRaw != "" && replyToCidRaw != "" { 462 - uri, err := syntax.ParseATURI(replyToUriRaw) 463 - if err != nil { 464 - rp.pages.Notice(w, "issue-comment", "reply-to-uri should be valid AT-URI") 465 - return 466 - } 467 - cid, err := syntax.ParseCID(replyToCidRaw) 468 - if err != nil { 469 - rp.pages.Notice(w, "issue-comment", "reply-to-cid should be valid CID") 470 - return 471 - } 472 - replyTo = &comatproto.RepoStrongRef{ 473 - Uri: uri.String(), 474 - Cid: cid.String(), 475 - } 476 - } 477 - 478 - mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 479 - 480 - comment := models.Comment{ 481 - Did: syntax.DID(user.Did), 482 - Collection: tangled.FeedCommentNSID, 483 - Rkey: syntax.RecordKey(tid.TID()), 484 - 485 - Subject: issueStrongRef, 486 - Body: markdownBody, 487 - Created: time.Now(), 488 - ReplyTo: replyTo, 489 - } 490 - if err = comment.Validate(); err != nil { 491 - l.Error("failed to validate comment", "err", err) 492 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 493 - return 494 - } 495 - 496 - client, err := rp.oauth.AuthorizedClient(r) 497 - if err != nil { 498 - l.Error("failed to get authorized client", "err", err) 499 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 500 - return 501 - } 502 - 503 - // create a record first 504 - out, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 505 - Collection: comment.Collection.String(), 506 - Repo: comment.Did.String(), 507 - Rkey: comment.Rkey.String(), 508 - Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, 509 - }) 510 - if err != nil { 511 - l.Error("failed to create comment", "err", err) 512 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 513 - return 514 - } 515 - 516 - comment.Cid = syntax.CID(out.Cid) 517 - 518 - tx, err := rp.db.Begin() 519 - if err != nil { 520 - l.Error("failed to start transaction", "err", err) 521 - rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.") 522 - return 523 - } 524 - defer tx.Rollback() 525 - 526 - err = db.PutComment(tx, &comment, references) 527 - if err != nil { 528 - l.Error("failed to create comment", "err", err) 529 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 530 - return 531 - } 532 - 533 - err = tx.Commit() 534 - if err != nil { 535 - l.Error("failed to commit transaction", "err", err) 536 - rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.") 537 - return 538 - } 539 - 540 - rp.notifier.NewComment(r.Context(), &comment, mentions) 541 - 542 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 543 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, comment.Id)) 544 - } 545 - 546 - func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { 547 - l := rp.logger.With("handler", "IssueComment") 548 - user := rp.oauth.GetMultiAccountUser(r) 549 - 550 - issue, ok := r.Context().Value("issue").(*models.Issue) 551 - if !ok { 552 - l.Error("failed to get issue") 553 - rp.pages.Error404(w) 554 - return 555 - } 556 - 557 - commentId := chi.URLParam(r, "commentId") 558 - comments, err := db.GetComments( 559 - rp.db, 560 - orm.FilterEq("id", commentId), 561 - ) 562 - if err != nil { 563 - l.Error("failed to fetch comment", "id", commentId) 564 - http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 565 - return 566 - } 567 - if len(comments) != 1 { 568 - l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 569 - http.Error(w, "invalid comment id", http.StatusBadRequest) 570 - return 571 - } 572 - comment := comments[0] 573 - 574 - rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ 575 - LoggedInUser: user, 576 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 577 - Issue: issue, 578 - Comment: &comment, 579 - }) 580 - } 581 - 582 - func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { 583 - l := rp.logger.With("handler", "EditIssueComment") 584 - user := rp.oauth.GetMultiAccountUser(r) 585 - 586 - issue, ok := r.Context().Value("issue").(*models.Issue) 587 - if !ok { 588 - l.Error("failed to get issue") 589 - rp.pages.Error404(w) 590 - return 591 - } 592 - 593 - commentId := chi.URLParam(r, "commentId") 594 - comments, err := db.GetComments( 595 - rp.db, 596 - orm.FilterEq("id", commentId), 597 - ) 598 - if err != nil { 599 - l.Error("failed to fetch comment", "id", commentId) 600 - http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 601 - return 602 - } 603 - if len(comments) != 1 { 604 - l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 605 - http.Error(w, "invalid comment id", http.StatusBadRequest) 606 - return 607 - } 608 - comment := comments[0] 609 - 610 - if comment.Did.String() != user.Did { 611 - l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 612 - http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 613 - return 614 - } 615 - 616 - switch r.Method { 617 - case http.MethodGet: 618 - rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 619 - LoggedInUser: user, 620 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 621 - Issue: issue, 622 - Comment: &comment, 623 - }) 624 - case http.MethodPost: 625 - // extract form value 626 - body := r.FormValue("body") 627 - if body == "" { 628 - rp.pages.Notice(w, "issue-comment", "Body is required") 629 - return 630 - } 631 - 632 - // TODO(boltless): normalize markdown body 633 - normalizedBody := body 634 - _, references := rp.mentionsResolver.Resolve(r.Context(), body) 635 - 636 - now := time.Now() 637 - newComment := comment 638 - newComment.Body = tangled.MarkupMarkdown{ 639 - Text: normalizedBody, 640 - Original: &body, 641 - Blobs: nil, 642 - } 643 - newComment.Edited = &now 644 - 645 - client, err := rp.oauth.AuthorizedClient(r) 646 - if err != nil { 647 - l.Error("failed to get authorized client", "err", err) 648 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 649 - return 650 - } 651 - 652 - // update a record first 653 - exCid := comment.Cid.String() 654 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 655 - Collection: newComment.Collection.String(), 656 - Repo: newComment.Did.String(), 657 - Rkey: newComment.Rkey.String(), 658 - SwapRecord: &exCid, 659 - Record: &lexutil.LexiconTypeDecoder{ 660 - Val: newComment.AsRecord(), 661 - }, 662 - }) 663 - if err != nil { 664 - l.Error("failed to update comment", "err", err) 665 - rp.pages.Notice(w, "issue-comment", "Failed to update comment, try again later.") 666 - return 667 - } 668 - 669 - newComment.Cid = syntax.CID(resp.Cid) 670 - 671 - tx, err := rp.db.Begin() 672 - if err != nil { 673 - l.Error("failed to start transaction", "err", err) 674 - rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 675 - return 676 - } 677 - defer tx.Rollback() 678 - 679 - err = db.PutComment(tx, &newComment, references) 680 - if err != nil { 681 - l.Error("failed to perform update-description query", "err", err) 682 - rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 683 - return 684 - } 685 - err = tx.Commit() 686 - if err != nil { 687 - l.Error("failed to commit transaction", "err", err) 688 - rp.pages.Notice(w, "issue-comment", "Failed to update comment, try again later.") 689 - return 690 - } 691 - 692 - // return new comment body with htmx 693 - rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ 694 - LoggedInUser: user, 695 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 696 - Issue: issue, 697 - Comment: &newComment, 698 - }) 699 - } 700 - } 701 - 702 - func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { 703 - l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") 704 - user := rp.oauth.GetMultiAccountUser(r) 705 - 706 - issue, ok := r.Context().Value("issue").(*models.Issue) 707 - if !ok { 708 - l.Error("failed to get issue") 709 - rp.pages.Error404(w) 710 - return 711 - } 712 - 713 - commentId := chi.URLParam(r, "commentId") 714 - comments, err := db.GetComments( 715 - rp.db, 716 - orm.FilterEq("id", commentId), 717 - ) 718 - if err != nil { 719 - l.Error("failed to fetch comment", "id", commentId) 720 - http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 721 - return 722 - } 723 - if len(comments) != 1 { 724 - l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 725 - http.Error(w, "invalid comment id", http.StatusBadRequest) 726 - return 727 - } 728 - comment := comments[0] 729 - 730 - rp.pages.ReplyIssueCommentPlaceholderFragment(w, pages.ReplyIssueCommentPlaceholderParams{ 731 - LoggedInUser: user, 732 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 733 - Issue: issue, 734 - Comment: &comment, 735 - }) 736 - } 737 - 738 - func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) { 739 - l := rp.logger.With("handler", "ReplyIssueComment") 740 - user := rp.oauth.GetMultiAccountUser(r) 741 - 742 - issue, ok := r.Context().Value("issue").(*models.Issue) 743 - if !ok { 744 - l.Error("failed to get issue") 745 - rp.pages.Error404(w) 746 - return 747 - } 748 - 749 - commentId := chi.URLParam(r, "commentId") 750 - comments, err := db.GetComments( 751 - rp.db, 752 - orm.FilterEq("id", commentId), 753 - ) 754 - if err != nil { 755 - l.Error("failed to fetch comment", "id", commentId) 756 - http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 757 - return 758 - } 759 - if len(comments) != 1 { 760 - l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 761 - http.Error(w, "invalid comment id", http.StatusBadRequest) 762 - return 763 - } 764 - comment := comments[0] 765 - 766 - rp.pages.ReplyIssueCommentFragment(w, pages.ReplyIssueCommentParams{ 767 - LoggedInUser: user, 768 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 769 - Issue: issue, 770 - Comment: &comment, 771 - }) 772 - } 773 - 774 - func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 775 - l := rp.logger.With("handler", "DeleteIssueComment") 776 - user := rp.oauth.GetMultiAccountUser(r) 777 - 778 - issue, ok := r.Context().Value("issue").(*models.Issue) 779 - if !ok { 780 - l.Error("failed to get issue") 781 - rp.pages.Error404(w) 782 - return 783 - } 784 - 785 - commentId := chi.URLParam(r, "commentId") 786 - comments, err := db.GetComments( 787 - rp.db, 788 - orm.FilterEq("id", commentId), 789 - ) 790 - if err != nil { 791 - l.Error("failed to fetch comment", "id", commentId) 792 - http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 793 - return 794 - } 795 - if len(comments) != 1 { 796 - l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 797 - http.Error(w, "invalid comment id", http.StatusBadRequest) 798 - return 799 - } 800 - comment := comments[0] 801 - 802 - if comment.Did.String() != user.Did { 803 - l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 804 - http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 805 - return 806 - } 807 - 808 - if comment.Deleted != nil { 809 - http.Error(w, "comment already deleted", http.StatusBadRequest) 810 - return 811 - } 812 - 813 - // optimistic deletion 814 - deleted := time.Now() 815 - err = db.DeleteComments(rp.db, orm.FilterEq("id", comment.Id)) 816 - if err != nil { 817 - l.Error("failed to delete comment", "err", err) 818 - rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 819 - return 820 - } 821 - 822 - // delete from pds 823 - if comment.Rkey != "" { 824 - client, err := rp.oauth.AuthorizedClient(r) 825 - if err != nil { 826 - l.Error("failed to get authorized client", "err", err) 827 - rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 828 - return 829 - } 830 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 831 - Collection: comment.Collection.String(), 832 - Repo: comment.Did.String(), 833 - Rkey: comment.Rkey.String(), 834 - }) 835 - if err != nil { 836 - l.Error("failed to delete from PDS", "err", err) 837 - } 838 - } 839 - 840 - // optimistic update for htmx 841 - comment.Body = tangled.MarkupMarkdown{} 842 - comment.Deleted = &deleted 843 - 844 - // htmx fragment of comment after deletion 845 - rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ 846 - LoggedInUser: user, 847 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 848 - Issue: issue, 849 - Comment: &comment, 850 - }) 851 - } 852 - 853 393 func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { 854 394 l := rp.logger.With("handler", "RepoIssues") 855 395
-9
appview/issues/router.go
··· 21 21 // authenticated routes 22 22 r.Group(func(r chi.Router) { 23 23 r.Use(middleware.AuthMiddleware(i.oauth)) 24 - r.Post("/comment", i.NewIssueComment) 25 - r.Route("/comment/{commentId}/", func(r chi.Router) { 26 - r.Get("/", i.IssueComment) 27 - r.Delete("/", i.DeleteIssueComment) 28 - r.Get("/edit", i.EditIssueComment) 29 - r.Post("/edit", i.EditIssueComment) 30 - r.Get("/reply", i.ReplyIssueComment) 31 - r.Get("/replyPlaceholder", i.ReplyIssueCommentPlaceholder) 32 - }) 33 24 r.Get("/edit", i.EditIssue) 34 25 r.Post("/edit", i.EditIssue) 35 26 r.Delete("/", i.DeleteIssue)
+5 -5
appview/models/comment.go
··· 31 31 Deleted *time.Time 32 32 } 33 33 34 - func (c *Comment) AtUri() syntax.ATURI { 34 + func (c Comment) AtUri() syntax.ATURI { 35 35 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", c.Did, c.Collection, c.Rkey)) 36 36 } 37 37 38 - func (c *Comment) StrongRef() comatproto.RepoStrongRef { 38 + func (c Comment) StrongRef() comatproto.RepoStrongRef { 39 39 return comatproto.RepoStrongRef{ 40 40 Uri: c.AtUri().String(), 41 41 Cid: c.Cid.String(), 42 42 } 43 43 } 44 44 45 - func (c *Comment) AsRecord() typegen.CBORMarshaler { 45 + func (c Comment) AsRecord() typegen.CBORMarshaler { 46 46 // can't convert to record for legacy types 47 47 if c.Collection != tangled.FeedCommentNSID { 48 48 return nil ··· 60 60 } 61 61 } 62 62 63 - func (c *Comment) EditableBody() string { 63 + func (c Comment) EditableBody() string { 64 64 if c.Body.Original != nil { 65 65 return *c.Body.Original 66 66 } 67 67 return c.Body.Text 68 68 } 69 69 70 - func (c *Comment) IsLegacy() bool { 70 + func (c Comment) IsLegacy() bool { 71 71 return c.Collection != tangled.FeedCommentNSID 72 72 } 73 73
+32 -1
appview/pages/pages.go
··· 1213 1213 type IssueCommentBodyParams struct { 1214 1214 LoggedInUser *oauth.MultiAccountUser 1215 1215 RepoInfo repoinfo.RepoInfo 1216 - Issue *models.Issue 1217 1216 Comment *models.Comment 1218 1217 } 1219 1218 ··· 1624 1623 return p.execute("timeline/home", w, params) 1625 1624 } 1626 1625 1626 + type CommentBodyFragmentParams struct { 1627 + Comment models.Comment 1628 + } 1629 + 1630 + func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1631 + return p.executePlain("fragments/comment/commentBody", w, params) 1632 + } 1633 + 1634 + type EditCommentFragmentParams struct { 1635 + Comment models.Comment 1636 + } 1637 + 1638 + func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1639 + return p.executePlain("fragments/comment/edit", w, params) 1640 + } 1641 + 1642 + type ReplyCommentFragmentParams struct { 1643 + LoggedInUser *oauth.MultiAccountUser 1644 + } 1645 + 1646 + func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1647 + return p.executePlain("fragments/comment/reply", w, params) 1648 + } 1649 + 1650 + type ReplyPlaceholderFragmentParams struct { 1651 + LoggedInUser *oauth.MultiAccountUser 1652 + } 1653 + 1654 + func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1655 + return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1656 + } 1657 + 1627 1658 func (p *Pages) Static() http.Handler { 1628 1659 if p.dev { 1629 1660 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
+2 -2
appview/pages/templates/repo/issues/fragments/issueCommentBody.html appview/pages/templates/fragments/comment/commentBody.html
··· 1 - {{ define "repo/issues/fragments/issueCommentBody" }} 2 - <div id="comment-body-{{.Comment.Id}}"> 1 + {{ define "fragments/comment/commentBody" }} 2 + <div class="comment-body"> 3 3 {{ if not .Comment.Deleted }} 4 4 <div class="prose dark:prose-invert">{{ .Comment.Body.Text | markdown }}</div> 5 5 {{ else }}
+13 -13
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html appview/pages/templates/fragments/comment/commentHeader.html
··· 1 - {{ define "repo/issues/fragments/issueCommentHeader" }} 2 - <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 1 + {{ define "fragments/comment/commentHeader" }} 2 + <div 3 + class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 " 4 + hx-target="next .comment-body" 5 + > 3 6 {{ $handle := resolve .Comment.Did.String }} 4 7 <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 5 8 {{ template "hats" $ }} ··· 8 11 {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }} 9 12 {{ if and $isCommentOwner (not .Comment.Deleted) }} 10 13 {{ if not .Comment.IsLegacy }} 11 - {{ template "editIssueComment" . }} 14 + {{ template "editCommentBtn" . }} 12 15 {{ end }} 13 - {{ template "deleteIssueComment" . }} 16 + {{ template "deleteCommentBtn" . }} 14 17 {{ end }} 15 18 </div> 16 19 {{ end }} ··· 36 39 </a> 37 40 {{ end }} 38 41 39 - {{ define "editIssueComment" }} 42 + {{ define "editCommentBtn" }} 40 43 <a 41 44 class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 42 - hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit" 43 - hx-swap="outerHTML" 44 - hx-target="#comment-body-{{.Comment.Id}}"> 45 + hx-get="/comment/edit?aturi={{ .Comment.AtUri }}" 46 + > 45 47 {{ i "pencil" "size-3 inline group-[.htmx-request]:hidden" }} 46 48 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 47 49 </a> 48 50 {{ end }} 49 51 50 - {{ define "deleteIssueComment" }} 52 + {{ define "deleteCommentBtn" }} 51 53 <a 52 54 class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 53 - hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/" 55 + hx-delete="/comment?aturi={{ .Comment.AtUri }}" 54 56 hx-confirm="Are you sure you want to delete your comment?" 55 - hx-swap="outerHTML" 56 - hx-target="#comment-body-{{.Comment.Id}}" 57 - > 57 + > 58 58 {{ i "trash-2" "size-3 inline group-[.htmx-request]:hidden" }} 59 59 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 60 60 </a>
+18 -13
appview/pages/templates/repo/issues/fragments/editIssueComment.html appview/pages/templates/fragments/comment/edit.html
··· 1 - {{ define "repo/issues/fragments/editIssueComment" }} 2 - <div id="comment-body-{{.Comment.Id}}" class="pt-2"> 1 + {{ define "fragments/comment/edit" }} 2 + <form 3 + class="pt-2" 4 + hx-patch="/comment" 5 + hx-swap="outerHTML" 6 + hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:find textarea" 7 + hx-indicator="find button[type='submit']" 8 + hx-disabled-elt="find button[type='submit']" 9 + > 10 + <input name="aturi" type="hidden" value="{{ .Comment.AtUri }}"> 3 11 <textarea 4 - id="edit-textarea-{{ .Comment.Id }}" 5 12 name="body" 6 13 class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 7 14 rows="5" 8 15 autofocus>{{ .Comment.EditableBody }}</textarea> 9 - 16 + <div id="comment-error" class="error"></div> 10 17 {{ template "editActions" $ }} 11 - </div> 18 + </form> 12 19 {{ end }} 13 20 14 21 {{ define "editActions" }} ··· 20 27 21 28 {{ define "save" }} 22 29 <button 30 + type="submit" 23 31 class="btn-create py-0 flex gap-1 items-center group text-sm" 24 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit" 25 - hx-trigger="click, keydown[(ctrlKey || metaKey) && key=='Enter'] from:#edit-textarea-{{ .Comment.Id }}" 26 - hx-include="#edit-textarea-{{ .Comment.Id }}" 27 - hx-target="#comment-body-{{ .Comment.Id }}" 28 - hx-swap="outerHTML"> 32 + > 29 33 {{ i "check" "size-4" }} 30 34 save 31 35 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} ··· 35 39 {{ define "cancel" }} 36 40 <button 37 41 class="btn py-0 text-red-500 dark:text-red-400 flex gap-1 items-center group" 38 - hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/" 39 - hx-target="#comment-body-{{ .Comment.Id }}" 40 - hx-swap="outerHTML"> 42 + hx-get="/comment?aturi={{ .Comment.AtUri }}" 43 + hx-target="closest form" 44 + hx-swap="outerHTML" 45 + > 41 46 {{ i "x" "size-4" }} 42 47 cancel 43 48 </button>
+50
appview/pages/templates/fragments/comment/reply.html
··· 1 + {{ define "fragments/comment/reply" }} 2 + <form 3 + class="p-2 group w-full border-t border-gray-200 dark:border-gray-700 flex flex-col gap-2" 4 + hx-post="/comment" 5 + hx-swap="none" 6 + hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:find textarea" 7 + hx-on::after-request="if(event.detail.successful) this.reset()" 8 + hx-disabled-elt="find button[type='submit']" 9 + > 10 + {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 11 + <textarea 12 + name="body" 13 + class="w-full p-2" 14 + placeholder="Leave a reply..." 15 + autofocus 16 + rows="3"></textarea> 17 + <div id="comment-error" class="error"></div> 18 + {{ template "replyActions" . }} 19 + </form> 20 + {{ end }} 21 + 22 + {{ define "replyActions" }} 23 + <div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm"> 24 + {{ template "cancel" . }} 25 + {{ template "reply" . }} 26 + </div> 27 + {{ end }} 28 + 29 + {{ define "cancel" }} 30 + <button 31 + class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group" 32 + hx-get="/comment/reply/placeholder" 33 + hx-target="closest form" 34 + hx-swap="outerHTML" 35 + > 36 + {{ i "x" "size-4" }} 37 + cancel 38 + </button> 39 + {{ end }} 40 + 41 + {{ define "reply" }} 42 + <button 43 + type="submit" 44 + class="btn-create flex items-center gap-2 no-underline hover:no-underline" 45 + > 46 + {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 47 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 48 + reply 49 + </button> 50 + {{ end }}
+15
appview/pages/templates/fragments/comment/replyPlaceholder.html
··· 1 + {{ define "fragments/comment/replyPlaceholder" }} 2 + <div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 + {{ if .LoggedInUser }} 4 + {{ template "user/fragments/pic" (list .LoggedInUser.Did "size-8 mr-1") }} 5 + {{ end }} 6 + <input 7 + class="w-full p-0 border-none focus:outline-none bg-transparent" 8 + placeholder="Leave a reply..." 9 + hx-get="/comment/reply" 10 + hx-trigger="focus" 11 + hx-target="closest div" 12 + hx-swap="outerHTML" 13 + > 14 + </div> 15 + {{ end }}
+23 -15
appview/pages/templates/repo/issues/fragments/commentList.html
··· 1 1 {{ define "repo/issues/fragments/commentList" }} 2 2 <div class="flex flex-col gap-4"> 3 3 {{ range $item := .CommentList }} 4 - {{ template "commentListing" (list $ .) }} 4 + {{ template "commentListItem" (list $ .) }} 5 5 {{ end }} 6 6 </div> 7 7 {{ end }} 8 8 9 - {{ define "commentListing" }} 9 + {{ define "commentListItem" }} 10 10 {{ $root := index . 0 }} 11 - {{ $comment := index . 1 }} 11 + {{ $item := index . 1 }} 12 12 {{ $params := 13 13 (dict 14 - "RepoInfo" $root.RepoInfo 15 14 "LoggedInUser" $root.LoggedInUser 16 - "Issue" $root.Issue 17 - "Comment" $comment.Self) }} 15 + "Comment" $item.Self) }} 18 16 19 17 <div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50"> 20 18 {{ template "topLevelComment" $params }} 21 19 22 20 <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 23 - {{ range $index, $reply := $comment.Replies }} 21 + {{ range $index, $reply := $item.Replies }} 24 22 <div class="-ml-4"> 25 23 {{ 26 24 template "replyComment" 27 25 (dict 28 - "RepoInfo" $root.RepoInfo 29 26 "LoggedInUser" $root.LoggedInUser 30 - "Issue" $root.Issue 31 - "Comment" $reply) 27 + "Comment" $reply) 32 28 }} 33 29 </div> 34 30 {{ end }} 35 31 </div> 36 32 37 - {{ template "repo/issues/fragments/replyIssueCommentPlaceholder" $params }} 33 + <div hx-include="this"> 34 + <input name="subject-uri" type="hidden" value="{{ $item.Self.Subject.Uri }}"> 35 + <input name="subject-cid" type="hidden" value="{{ $item.Self.Subject.Cid }}"> 36 + <input name="reply-to-uri" type="hidden" value="{{ $item.Self.AtUri }}"> 37 + <input name="reply-to-cid" type="hidden" value="{{ $item.Self.Cid }}"> 38 + {{ if $item.Self.IsLegacy }} 39 + <div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 40 + <span class="text-orange-500">Can't reply to legacy comment.</span> 41 + </div> 42 + {{ else }} 43 + {{ template "fragments/comment/replyPlaceholder" (dict "LoggedInUser" $root.LoggedInUser) }} 44 + {{ end }} 45 + </div> 38 46 </div> 39 47 {{ end }} 40 48 ··· 44 52 {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 45 53 </div> 46 54 <div class="flex-1 min-w-0"> 47 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 48 - {{ template "repo/issues/fragments/issueCommentBody" . }} 55 + {{ template "fragments/comment/commentHeader" . }} 56 + {{ template "fragments/comment/commentBody" . }} 49 57 </div> 50 58 </div> 51 59 {{ end }} ··· 56 64 {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }} 57 65 </div> 58 66 <div class="flex-1 min-w-0"> 59 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 60 - {{ template "repo/issues/fragments/issueCommentBody" . }} 67 + {{ template "fragments/comment/commentHeader" . }} 68 + {{ template "fragments/comment/commentBody" . }} 61 69 </div> 62 70 </div> 63 71 {{ end }}
+16 -37
appview/pages/templates/repo/issues/fragments/newComment.html
··· 1 1 {{ define "repo/issues/fragments/newComment" }} 2 2 {{ if .LoggedInUser }} 3 3 <form 4 - id="comment-form" 5 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 6 - hx-trigger="submit, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 7 - hx-disabled-elt="#comment-form button" 8 - hx-on::after-request="if(event.detail.successful) this.reset()" 9 - class="group/form" 4 + hx-post="/comment" 5 + hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 6 + hx-disabled-elt="find button[type='submit']" 7 + hx-on::after-request="if(event.detail.successful) this.reset()" 8 + class="group/form" 10 9 > 10 + <input name="subject-uri" type="hidden" value="{{ .Issue.AtUri }}"> 11 11 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full"> 12 12 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 13 13 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 14 14 </div> 15 - <textarea 16 - id="comment-textarea" 17 - name="body" 18 - class="w-full p-2 rounded" 19 - placeholder="Add to the discussion. Markdown is supported." 20 - onkeyup="updateCommentForm()" 21 - rows="5" 22 - ></textarea> 23 - <div id="issue-comment"></div> 15 + <textarea 16 + id="comment-textarea" 17 + name="body" 18 + class="w-full p-2 rounded" 19 + placeholder="Add to the discussion. Markdown is supported." 20 + onkeyup="updateCommentForm()" 21 + rows="5" 22 + required 23 + ></textarea> 24 + <div id="comment-error" class="error"></div> 24 25 <div id="issue-action" class="error"></div> 25 26 </div> 26 27 ··· 50 51 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]/close:inline" }} 51 52 <span id="close-button-text">close</span> 52 53 </button> 53 - <div 54 - id="close-with-comment" 55 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 56 - hx-trigger="click from:#close-button" 57 - hx-disabled-elt="#close-with-comment" 58 - hx-target="#issue-comment" 59 - hx-vals="js:{body: document.getElementById('comment-textarea').value.trim() !== '' ? document.getElementById('comment-textarea').value : ''}" 60 - hx-swap="none" 61 - hx-indicator="#close-button" 62 - > 63 - </div> 64 54 <div 65 55 id="close-issue" 66 56 hx-disabled-elt="#close-issue" ··· 71 61 hx-indicator="#close-button" 72 62 > 73 63 </div> 74 - <script> 75 - document.addEventListener('htmx:configRequest', function(evt) { 76 - if (evt.target.id === 'close-with-comment') { 77 - const commentText = document.getElementById('comment-textarea').value.trim(); 78 - if (commentText === '') { 79 - evt.detail.parameters = {}; 80 - evt.preventDefault(); 81 - } 82 - } 83 - }); 84 - </script> 85 64 {{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (not .Issue.Open) }} 86 65 <button 87 66 type="button"
-66
appview/pages/templates/repo/issues/fragments/replyComment.html
··· 1 - {{ define "repo/issues/fragments/replyComment" }} 2 - <form 3 - class="p-2 group w-full border-t border-gray-200 dark:border-gray-700 flex flex-col gap-2" 4 - id="reply-form-{{ .Comment.Id }}" 5 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 6 - hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:#reply-{{.Comment.Id}}-textarea" 7 - hx-on::after-request="if(event.detail.successful) this.reset()" 8 - hx-disabled-elt="#reply-{{ .Comment.Id }}" 9 - > 10 - {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 11 - <textarea 12 - id="reply-{{.Comment.Id}}-textarea" 13 - name="body" 14 - class="w-full p-2" 15 - placeholder="Leave a reply..." 16 - autofocus 17 - rows="3"></textarea> 18 - 19 - <input 20 - type="text" 21 - id="reply-to-uri" 22 - name="reply-to-uri" 23 - required 24 - value="{{ .Comment.AtUri }}" 25 - class="hidden" 26 - /> 27 - <input 28 - type="text" 29 - id="reply-to-cid" 30 - name="reply-to-cid" 31 - required 32 - value="{{ .Comment.Cid }}" 33 - class="hidden" 34 - /> 35 - {{ template "replyActions" . }} 36 - </form> 37 - {{ end }} 38 - 39 - {{ define "replyActions" }} 40 - <div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm"> 41 - {{ template "cancel" . }} 42 - {{ template "reply" . }} 43 - </div> 44 - {{ end }} 45 - 46 - {{ define "cancel" }} 47 - <button 48 - class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group" 49 - hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/replyPlaceholder" 50 - hx-target="#reply-form-{{ .Comment.Id }}" 51 - hx-swap="outerHTML"> 52 - {{ i "x" "size-4" }} 53 - cancel 54 - </button> 55 - {{ end }} 56 - 57 - {{ define "reply" }} 58 - <button 59 - id="reply-{{ .Comment.Id }}" 60 - type="submit" 61 - class="btn-create flex items-center gap-2 no-underline hover:no-underline"> 62 - {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 63 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 64 - reply 65 - </button> 66 - {{ end }}
-22
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
··· 1 - {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} 2 - <div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 - {{ if .Comment.IsLegacy }} 4 - {{ if .LoggedInUser }} 5 - <span class="text-orange-500">Can't reply to legacy comment.</span> 6 - {{ end }} 7 - {{ else }} 8 - {{ if .LoggedInUser }} 9 - {{ template "user/fragments/pic" (list .LoggedInUser.Did "size-8 mr-1") }} 10 - {{ end }} 11 - <input 12 - class="w-full p-0 border-none focus:outline-none bg-transparent" 13 - placeholder="Leave a reply..." 14 - hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 15 - hx-trigger="focus" 16 - hx-target="closest div" 17 - hx-swap="outerHTML" 18 - > 19 - </input> 20 - {{ end }} 21 - </div> 22 - {{ end }}
+8 -18
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 22 22 {{ $isLastRound := eq $roundNumber $lastIdx }} 23 23 {{ $isSameRepoBranch := .Pull.IsBranchBased }} 24 24 {{ $isUpToDate := .ResubmitCheck.No }} 25 - <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative p-2"> 25 + <div id="actions-{{$roundNumber}}" hx-target="this" class="flex flex-wrap gap-2 relative p-2"> 26 26 <button 27 27 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 28 - hx-target="#actions-{{$roundNumber}}" 29 - hx-swap="outerHtml" 30 28 class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 29 {{ i "message-square-plus" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 32 30 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} ··· 44 42 </button> 45 43 {{ end }} 46 44 {{ if and $isPushAllowed $isOpen $isLastRound }} 47 - {{ $disabled := "" }} 48 - {{ if $isConflicted }} 49 - {{ $disabled = "disabled" }} 50 - {{ end }} 51 45 <button 52 46 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 53 47 hx-swap="none" 54 48 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 55 - class="btn-flat p-2 flex items-center gap-2 group" {{ $disabled }}> 49 + class="btn-flat p-2 flex items-center gap-2 group" 50 + {{ if $isConflicted }}disabled{{ end }} 51 + > 56 52 {{ i "git-merge" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 57 53 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 54 merge{{if $stackCount}} {{$stackCount}}{{end}} ··· 60 56 {{ end }} 61 57 62 58 {{ if and $isPullAuthor $isOpen $isLastRound }} 63 - {{ $disabled := "" }} 64 - {{ if $isUpToDate }} 65 - {{ $disabled = "disabled" }} 66 - {{ end }} 67 59 <button id="resubmitBtn" 68 60 {{ if not .Pull.IsPatchBased }} 69 61 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 62 + hx-swap="none" 70 63 {{ else }} 71 64 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 72 - hx-target="#actions-{{$roundNumber}}" 73 - hx-swap="outerHtml" 74 65 {{ end }} 75 66 76 67 hx-disabled-elt="#resubmitBtn" 77 - class="btn-flat p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 68 + class="btn-flat p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" 78 69 79 - {{ if $disabled }} 70 + {{ if not $isUpToDate }} 80 71 title="Update this branch to resubmit this pull request" 72 + disabled 81 73 {{ else }} 82 74 title="Resubmit this pull request" 83 75 {{ end }} ··· 111 103 {{ end }} 112 104 </div> 113 105 {{ end }} 114 - 115 -
+12 -14
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 1 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 - <div 3 - id="pull-comment-card-{{ .RoundNumber }}" 4 - class="w-full flex flex-col gap-2"> 2 + <div class="w-full flex flex-col gap-2"> 5 3 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 6 4 <form 7 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 8 - hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:#pull-comment-textarea" 5 + class="w-full flex flex-wrap gap-2 group" 6 + hx-post="/comment" 7 + hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:find textarea" 9 8 hx-swap="none" 9 + hx-indicator="find button[type='submit']" 10 + hx-disabled-elt="find button[type='submit']" 10 11 hx-on::after-request="if(event.detail.successful) this.reset()" 11 - hx-disabled-elt="#reply-{{ .RoundNumber }}" 12 - class="w-full flex flex-wrap gap-2 group" 13 12 > 13 + <input name="subject-uri" type="hidden" value="{{ .Pull.AtUri }}"> 14 + <input name="pull-round-idx" type="hidden" value="{{ .RoundNumber }}"> 14 15 <textarea 15 - id="pull-comment-textarea" 16 - name="body" 17 - class="w-full p-2 rounded border" 18 - rows=8 19 - placeholder="Add to the discussion..."></textarea 16 + name="body" 17 + class="w-full p-2 rounded border" 18 + rows=8 19 + placeholder="Add to the discussion..."></textarea 20 20 > 21 21 {{ template "replyActions" . }} 22 22 <div id="pull-comment"></div> ··· 47 47 {{ define "reply" }} 48 48 <button 49 49 type="submit" 50 - id="reply-{{ .RoundNumber }}" 51 50 class="btn-create flex items-center gap-2"> 52 51 {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 53 52 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 54 53 reply 55 54 </button> 56 55 {{ end }} 57 -
+19 -2
appview/pages/templates/repo/pulls/pull.html
··· 592 592 </summary> 593 593 <div> 594 594 {{ range $item.Comments }} 595 - {{ template "submissionComment" . }} 595 + {{/* template "submissionComment" . */}} 596 + {{ template "comment" (dict "LoggedInUser" $root.LoggedInUser "Comment" .) }} 596 597 {{ end }} 597 598 </div> 598 599 {{ if gt $c 0}} ··· 607 608 {{ block "resubmitStatus" $root }} {{ end }} 608 609 {{ end }} 609 610 </div> 610 - <div class="relative -ml-10 bg-gray-50 dark:bg-gray-900"> 611 + <div hx-include="this" class="relative -ml-10 bg-gray-50 dark:bg-gray-900"> 611 612 {{ if $root.LoggedInUser }} 613 + <input name="subject-uri" type="hidden" value="{{ $root.Pull.AtUri }}"> 614 + <input name="pull-round-idx" type="hidden" value="{{ $item.RoundNumber }}"> 612 615 {{ template "repo/pulls/fragments/pullActions" 613 616 (dict 614 617 "LoggedInUser" $root.LoggedInUser ··· 624 627 </details> 625 628 {{ end }} 626 629 630 + {{ define "comment" }} 631 + <div class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 632 + <!-- left column: profile picture --> 633 + <div class="flex-shrink-0 h-fit relative"> 634 + {{ template "user/fragments/picLink" (list .Comment.Did.String "size-8") }} 635 + </div> 636 + <!-- right column: name and body in two rows --> 637 + <div class="flex-1 min-w-0"> 638 + {{ template "fragments/comment/commentHeader" . }} 639 + {{ template "fragments/comment/commentBody" . }} 640 + </div> 641 + </div> 642 + {{ end }} 643 + 627 644 {{ define "submissionComment" }} 628 645 <div id="comment-{{.Id}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto group/comment"> 629 646 <!-- left column: profile picture -->
+4 -125
appview/pulls/pulls.go
··· 288 288 diff = patchutil.Interdiff(previousPatch, currentPatch) 289 289 } 290 290 291 - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 291 + err = s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 292 292 LoggedInUser: user, 293 293 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 294 294 Pull: pull, ··· 308 308 309 309 LabelDefs: defs, 310 310 }) 311 + if err != nil { 312 + s.logger.Error("failed to render", "err", err) 313 + } 311 314 } 312 315 313 316 func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { ··· 804 807 l = l.With("user", user.Did) 805 808 } 806 809 807 - f, err := s.repoResolver.Resolve(r) 808 - if err != nil { 809 - l.Error("failed to get repo and knot", "err", err) 810 - return 811 - } 812 - 813 810 pull, ok := r.Context().Value("pull").(*models.Pull) 814 811 if !ok { 815 812 l.Error("failed to get pull") ··· 835 832 RoundNumber: roundNumber, 836 833 }) 837 834 return 838 - case http.MethodPost: 839 - body := r.FormValue("body") 840 - if body == "" { 841 - s.pages.Notice(w, "pull-comment", "Comment body is required") 842 - return 843 - } 844 - 845 - // TODO(boltless): normalize markdown body 846 - normalizedBody := body 847 - mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 848 - 849 - markdownBody := tangled.MarkupMarkdown{ 850 - Text: normalizedBody, 851 - Original: &body, 852 - Blobs: nil, 853 - } 854 - 855 - // ingest CID of PR record on-demand. 856 - // TODO(boltless): appview should ingest CID of atproto records 857 - cid, err := func() (syntax.CID, error) { 858 - ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 859 - if err != nil { 860 - return "", err 861 - } 862 - 863 - xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} 864 - out, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoPullNSID, pull.OwnerDid, pull.Rkey) 865 - if err != nil { 866 - return "", err 867 - } 868 - if out.Cid == nil { 869 - return "", fmt.Errorf("record CID is empty") 870 - } 871 - 872 - cid, err := syntax.ParseCID(*out.Cid) 873 - if err != nil { 874 - return "", err 875 - } 876 - 877 - return cid, nil 878 - }() 879 - if err != nil { 880 - s.logger.Error("failed to backfill subject PR record", "err", err) 881 - s.pages.Notice(w, "pull-comment", "failed to backfill subject record") 882 - return 883 - } 884 - pullStrongRef := comatproto.RepoStrongRef{ 885 - Uri: pull.AtUri().String(), 886 - Cid: cid.String(), 887 - } 888 - 889 - comment := models.Comment{ 890 - Did: syntax.DID(user.Did), 891 - Collection: tangled.FeedCommentNSID, 892 - Rkey: syntax.RecordKey(tid.TID()), 893 - 894 - Subject: pullStrongRef, 895 - Body: markdownBody, 896 - Created: time.Now(), 897 - ReplyTo: nil, 898 - PullRoundIdx: &roundNumber, 899 - } 900 - if err = comment.Validate(); err != nil { 901 - s.logger.Error("failed to validate comment", "err", err) 902 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 903 - return 904 - } 905 - 906 - client, err := s.oauth.AuthorizedClient(r) 907 - if err != nil { 908 - s.logger.Error("failed to get authorized client", "err", err) 909 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 910 - return 911 - } 912 - 913 - out, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 914 - Collection: comment.Collection.String(), 915 - Repo: comment.Did.String(), 916 - Rkey: comment.Rkey.String(), 917 - Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, 918 - }) 919 - if err != nil { 920 - s.logger.Error("failed to create pull comment", "err", err) 921 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 922 - return 923 - } 924 - 925 - comment.Cid = syntax.CID(out.Cid) 926 - 927 - // Start a transaction 928 - tx, err := s.db.BeginTx(r.Context(), nil) 929 - if err != nil { 930 - l.Error("failed to start transaction", "err", err) 931 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 932 - return 933 - } 934 - defer tx.Rollback() 935 - 936 - // Create the pull comment in the database 937 - err = db.PutComment(tx, &comment, references) 938 - if err != nil { 939 - l.Error("failed to create pull comment in database", "err", err) 940 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 941 - return 942 - } 943 - 944 - // Commit the transaction 945 - if err = tx.Commit(); err != nil { 946 - l.Error("failed to commit transaction", "err", err) 947 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 948 - return 949 - } 950 - 951 - s.notifier.NewComment(r.Context(), &comment, mentions) 952 - 953 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 954 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 955 - return 956 835 } 957 836 } 958 837
+1 -4
appview/pulls/router.go
··· 29 29 r.Get("/", s.RepoPullPatch) 30 30 r.Get("/interdiff", s.RepoPullInterdiff) 31 31 r.Get("/actions", s.PullActions) 32 - r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 33 - r.Get("/", s.PullComment) 34 - r.Post("/", s.PullComment) 35 - }) 32 + r.Get("/comment", s.PullComment) 36 33 }) 37 34 38 35 r.Route("/round/{round}.patch", func(r chi.Router) {
+396
appview/state/comment.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "strconv" 7 + "time" 8 + 9 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 + 14 + "tangled.org/core/api/tangled" 15 + "tangled.org/core/appview/db" 16 + "tangled.org/core/appview/models" 17 + "tangled.org/core/appview/pages" 18 + "tangled.org/core/orm" 19 + "tangled.org/core/tid" 20 + ) 21 + 22 + func (s *State) CommentBodyFragment(w http.ResponseWriter, r *http.Request) { 23 + l := s.logger.With("handler", "CommentBodyFragment") 24 + 25 + commentAt := r.URL.Query().Get("aturi") 26 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 27 + if err != nil { 28 + l.Error("failed to fetch comment", "aturi", commentAt) 29 + http.Error(w, "Failed to fetch comment", http.StatusInternalServerError) 30 + return 31 + } 32 + 33 + err = s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{ 34 + Comment: comment, 35 + }) 36 + if err != nil { 37 + l.Error("failed to render") 38 + } 39 + } 40 + 41 + func (s *State) EditCommentFragment(w http.ResponseWriter, r *http.Request) { 42 + l := s.logger.With("handler", "EditCommentFragment") 43 + 44 + commentAt := r.URL.Query().Get("aturi") 45 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 46 + if err != nil { 47 + l.Error("failed to fetch comment", "aturi", commentAt) 48 + http.Error(w, "Failed to fetch comment", http.StatusInternalServerError) 49 + return 50 + } 51 + 52 + err = s.pages.EditCommentFragment(w, pages.EditCommentFragmentParams{ 53 + Comment: comment, 54 + }) 55 + if err != nil { 56 + l.Error("failed to render") 57 + } 58 + } 59 + 60 + func (s *State) NewReplyCommentFragment(w http.ResponseWriter, r *http.Request) { 61 + s.pages.ReplyCommentFragment(w, pages.ReplyCommentFragmentParams{ 62 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 63 + }) 64 + } 65 + 66 + func (s *State) ReplyPlaceholderFragment(w http.ResponseWriter, r *http.Request) { 67 + s.pages.ReplyPlaceholderFragment(w, pages.ReplyPlaceholderFragmentParams{ 68 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 69 + }) 70 + } 71 + 72 + func (s *State) NewComment(w http.ResponseWriter, r *http.Request) { 73 + l := s.logger.With("handler", "NewComment") 74 + user := s.oauth.GetMultiAccountUser(r) 75 + 76 + noticeId := "comment-error" 77 + ctx := r.Context() 78 + 79 + body := r.FormValue("body") 80 + if body == "" { 81 + s.pages.Notice(w, noticeId, "Body is required") 82 + return 83 + } 84 + 85 + // TODO(boltless): normalize markdown body 86 + normalizedBody := body 87 + _, references := s.mentionsResolver.Resolve(ctx, body) 88 + 89 + markdownBody := tangled.MarkupMarkdown{ 90 + Text: normalizedBody, 91 + Original: &body, 92 + Blobs: nil, 93 + } 94 + 95 + subjectUri, err := syntax.ParseATURI(r.FormValue("subject-uri")) 96 + if err != nil { 97 + l.Warn("invalid subject uri", "err", err) 98 + s.pages.Notice(w, noticeId, "Subject URI should be valid AT-URI") 99 + return 100 + } 101 + l = l.With("subject.uri", subjectUri) 102 + 103 + // ingest CID of subject record on-demand. 104 + // TODO(boltless): appview should ingest CID of all atproto records 105 + var subjectCid syntax.CID 106 + if subjectCidRaw := r.FormValue("subject-cid"); subjectCidRaw != "" { 107 + subjectCid, err = syntax.ParseCID(subjectCidRaw) 108 + if err != nil { 109 + l.Warn("invalid subject cid", "err", err) 110 + s.pages.Notice(w, noticeId, "Subject URI should be valid AT-URI") 111 + return 112 + } 113 + } else { 114 + l.Debug("ingesting subject record CID") 115 + subjectCid, err = func(uri syntax.ATURI) (syntax.CID, error) { 116 + ident, err := s.idResolver.ResolveIdent(ctx, uri.Authority().String()) 117 + if err != nil { 118 + return "", err 119 + } 120 + 121 + xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} 122 + out, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", uri.Collection().String(), ident.DID.String(), uri.RecordKey().String()) 123 + if err != nil { 124 + return "", err 125 + } 126 + if out.Cid == nil { 127 + return "", fmt.Errorf("record CID is empty") 128 + } 129 + 130 + cid, err := syntax.ParseCID(*out.Cid) 131 + if err != nil { 132 + return "", err 133 + } 134 + 135 + return cid, nil 136 + }(subjectUri) 137 + if err != nil { 138 + l.Error("failed to backfill subject record", "err", err) 139 + s.pages.Notice(w, noticeId, "failed to backfill subject record") 140 + return 141 + } 142 + } 143 + l = l.With("subject.cid", subjectCid) 144 + 145 + subject := comatproto.RepoStrongRef{ 146 + Uri: subjectUri.String(), 147 + Cid: subjectCid.String(), 148 + } 149 + 150 + var pullRoundIdx *int 151 + if pullRoundIdxRaw := r.FormValue("pull-round-idx"); pullRoundIdxRaw != "" { 152 + roundIdx, err := strconv.Atoi(pullRoundIdxRaw) 153 + if err != nil { 154 + l.Warn("invalid round idx", "err", err) 155 + s.pages.Notice(w, noticeId, "pull round index should be valid integer") 156 + return 157 + } 158 + pullRoundIdx = &roundIdx 159 + } 160 + 161 + var replyTo *comatproto.RepoStrongRef 162 + replyToUriRaw := r.FormValue("reply-to-uri") 163 + replyToCidRaw := r.FormValue("reply-to-cid") 164 + if replyToUriRaw != "" && replyToCidRaw != "" { 165 + uri, err := syntax.ParseATURI(replyToUriRaw) 166 + if err != nil { 167 + s.pages.Notice(w, noticeId, "reply-to-uri should be valid AT-URI") 168 + return 169 + } 170 + cid, err := syntax.ParseCID(replyToCidRaw) 171 + if err != nil { 172 + s.pages.Notice(w, noticeId, "reply-to-cid should be valid CID") 173 + return 174 + } 175 + replyTo = &comatproto.RepoStrongRef{ 176 + Uri: uri.String(), 177 + Cid: cid.String(), 178 + } 179 + } 180 + 181 + comment := models.Comment{ 182 + Did: syntax.DID(user.Did), 183 + Collection: tangled.FeedCommentNSID, 184 + Rkey: syntax.RecordKey(tid.TID()), 185 + 186 + Subject: subject, 187 + Body: markdownBody, 188 + Created: time.Now(), 189 + ReplyTo: replyTo, 190 + PullRoundIdx: pullRoundIdx, 191 + } 192 + if err = comment.Validate(); err != nil { 193 + l.Error("failed to validate comment", "err", err) 194 + s.pages.Notice(w, noticeId, "Failed to create comment.") 195 + return 196 + } 197 + 198 + client, err := s.oauth.AuthorizedClient(r) 199 + if err != nil { 200 + l.Error("failed to get authorized client", "err", err) 201 + s.pages.Notice(w, noticeId, "Failed to create comment.") 202 + return 203 + } 204 + 205 + // create a record first 206 + out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 207 + Collection: comment.Collection.String(), 208 + Repo: comment.Did.String(), 209 + Rkey: comment.Rkey.String(), 210 + Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, 211 + }) 212 + if err != nil { 213 + l.Error("failed to create comment", "err", err) 214 + s.pages.Notice(w, noticeId, "Failed to create comment.") 215 + return 216 + } 217 + 218 + comment.Cid = syntax.CID(out.Cid) 219 + 220 + tx, err := s.db.Begin() 221 + if err != nil { 222 + l.Error("failed to start transaction", "err", err) 223 + s.pages.Notice(w, noticeId, "Failed to create comment, try again later.") 224 + return 225 + } 226 + defer tx.Rollback() 227 + 228 + err = db.PutComment(tx, &comment, references) 229 + if err != nil { 230 + l.Error("failed to create comment", "err", err) 231 + s.pages.Notice(w, noticeId, "Failed to create comment.") 232 + return 233 + } 234 + 235 + err = tx.Commit() 236 + if err != nil { 237 + l.Error("failed to commit transaction", "err", err) 238 + s.pages.Notice(w, noticeId, "Failed to create comment, try again later.") 239 + return 240 + } 241 + 242 + // TODO: return comment or reply-comment fragment 243 + // onattach, htmx-callback to focus on comment. 244 + s.pages.HxRefresh(w) 245 + } 246 + 247 + func (s *State) EditComment(w http.ResponseWriter, r *http.Request) { 248 + l := s.logger.With("handler", "EditComment") 249 + user := s.oauth.GetMultiAccountUser(r) 250 + 251 + noticeId := "comment-error" 252 + ctx := r.Context() 253 + 254 + commentAt := r.FormValue("aturi") 255 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 256 + if err != nil { 257 + l.Error("failed to fetch comment", "aturi", commentAt, "err", err) 258 + s.pages.Notice(w, noticeId, "Failed to fetch comment") 259 + return 260 + } 261 + 262 + if comment.Did.String() != user.Did { 263 + l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 264 + s.pages.Notice(w, noticeId, "You are not the author of this comment") 265 + return 266 + } 267 + 268 + body := r.FormValue("body") 269 + if body == "" { 270 + s.pages.Notice(w, noticeId, "Body is required") 271 + return 272 + } 273 + 274 + // TODO(boltless): normalize markdown body 275 + normalizedBody := body 276 + _, references := s.mentionsResolver.Resolve(ctx, body) 277 + 278 + now := time.Now() 279 + newComment := comment 280 + newComment.Body = tangled.MarkupMarkdown{ 281 + Text: normalizedBody, 282 + Original: &body, 283 + Blobs: nil, 284 + } 285 + newComment.Edited = &now 286 + if err := newComment.Validate(); err != nil { 287 + l.Error("failed to validate comment", "err", err) 288 + s.pages.Notice(w, noticeId, "Failed to update comment.") 289 + return 290 + } 291 + 292 + client, err := s.oauth.AuthorizedClient(r) 293 + if err != nil { 294 + l.Error("failed to get authorized client", "err", err) 295 + s.pages.Notice(w, noticeId, "Failed to create comment. try again later.") 296 + return 297 + } 298 + 299 + // update the record first 300 + exCid := comment.Cid.String() 301 + out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 302 + Collection: newComment.Collection.String(), 303 + Repo: newComment.Did.String(), 304 + Rkey: newComment.Rkey.String(), 305 + SwapRecord: &exCid, 306 + Record: &lexutil.LexiconTypeDecoder{ 307 + Val: newComment.AsRecord(), 308 + }, 309 + }) 310 + if err != nil { 311 + l.Error("failed to update comment", "err", err) 312 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 313 + return 314 + } 315 + 316 + newComment.Cid = syntax.CID(out.Cid) 317 + 318 + tx, err := s.db.Begin() 319 + if err != nil { 320 + l.Error("failed to start transaction", "err", err) 321 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 322 + return 323 + } 324 + defer tx.Rollback() 325 + 326 + err = db.PutComment(tx, &newComment, references) 327 + if err != nil { 328 + l.Error("failed to perform update-description query", "err", err) 329 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 330 + return 331 + } 332 + err = tx.Commit() 333 + if err != nil { 334 + l.Error("failed to commit transaction", "err", err) 335 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 336 + return 337 + } 338 + 339 + // TODO: return full comment fragment so we can update comment header too 340 + s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{ 341 + Comment: newComment, 342 + }) 343 + } 344 + 345 + func (s *State) DeleteComment(w http.ResponseWriter, r *http.Request) { 346 + l := s.logger.With("handler", "DeleteComment") 347 + user := s.oauth.GetMultiAccountUser(r) 348 + 349 + noticeId := "comment" 350 + ctx := r.Context() 351 + 352 + commentAt := r.URL.Query().Get("aturi") 353 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 354 + if err != nil { 355 + l.Error("failed to fetch comment", "aturi", commentAt) 356 + s.pages.Notice(w, noticeId, "Failed to fetch comment.") 357 + return 358 + } 359 + 360 + if comment.Did.String() != user.Did { 361 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 362 + s.pages.Notice(w, noticeId, "you are not the author of this comment") 363 + return 364 + } 365 + 366 + if comment.Deleted != nil { 367 + s.pages.Notice(w, noticeId, "Comment already deleted") 368 + return 369 + } 370 + 371 + client, err := s.oauth.AuthorizedClient(r) 372 + if err != nil { 373 + l.Error("failed to get authorized client", "err", err) 374 + s.pages.Notice(w, "comment", "Failed to delete comment.") 375 + return 376 + } 377 + _, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 378 + Collection: comment.Collection.String(), 379 + Repo: comment.Did.String(), 380 + Rkey: comment.Rkey.String(), 381 + }) 382 + if err != nil { 383 + l.Error("failed to delete from PDS", "err", err) 384 + s.pages.Notice(w, noticeId, "Failed to delete comment, try again later.") 385 + return 386 + } 387 + 388 + // optimistic update for htmx response 389 + now := time.Now() 390 + comment.Body = tangled.MarkupMarkdown{} 391 + comment.Deleted = &now 392 + 393 + s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{ 394 + Comment: comment, 395 + }) 396 + }
+10
appview/state/router.go
··· 188 188 r.Delete("/", s.React) 189 189 }) 190 190 191 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 192 + r.Get("/", s.CommentBodyFragment) 193 + r.Get("/edit", s.EditCommentFragment) 194 + r.Get("/reply", s.NewReplyCommentFragment) 195 + r.Get("/reply/placeholder", s.ReplyPlaceholderFragment) 196 + r.Post("/", s.NewComment) 197 + r.Patch("/", s.EditComment) 198 + r.Delete("/", s.DeleteComment) 199 + }) 200 + 191 201 r.Route("/profile", func(r chi.Router) { 192 202 r.Use(middleware.AuthMiddleware(s.oauth)) 193 203 r.Get("/edit-bio", s.EditBioFragment)
-6
appview/strings/strings.go
··· 55 55 r.Get("/raw", s.contents) 56 56 r.Get("/edit", s.edit) 57 57 r.Post("/edit", s.edit) 58 - r. 59 - With(middleware.AuthMiddleware(s.OAuth)). 60 - Post("/comment", s.comment) 61 58 }) 62 59 }) 63 60 ··· 436 433 437 434 s.Pages.HxRedirect(w, "/strings/"+user.Did) 438 435 } 439 - 440 - func (s *Strings) comment(w http.ResponseWriter, r *http.Request) { 441 - }

History

10 rounds 4 comments
sign up or login to add to the discussion
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
no conflicts, ready to merge
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 4 comments

appview/pages/templates/fragments/comment/edit.html:2 now opens with form, I don't see a /form close in this diff tho

appview/state/comment.go:385-388 also how about a db.DeleteComments() from our sqlite?

appview/state/comment.go:249-268 comment.Validate() warranted just in case?

now opens with form, I don't see a /form close in this diff tho

how could I miss that. Thanks for pointing out.

how about a db.DeleteComments() from our sqlite?

I didn't do that on purpose. Comment deletion event will/should be ingested from appview ingester. For create/update events, it makes sense to do that on web handler because we can avoid parsing markdown again. But for delete event, we just don't need to delete records on web handler. Even if something goes wrong, it's ingester's job to retry, not web handlers.

comment.Validate() warranted just in case?

good idea. will add it.

1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments
1 commit
expand
appview: unified comment fragments/handlers
3/3 failed
expand
expand 0 comments