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
+635 -848
Diff #2
+11
appview/db/comments.go
··· 136 136 return err 137 137 } 138 138 139 + func GetComment(e Execer, filters ...orm.Filter) (models.Comment, error) { 140 + comments, err := GetComments(e, filters...) 141 + if err != nil { 142 + return models.Comment{}, err 143 + } 144 + if len(comments) != 1 { 145 + return models.Comment{}, fmt.Errorf("expected 1 comment, got %d", len(comments)) 146 + } 147 + return comments[0], nil 148 + } 149 + 139 150 func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) { 140 151 var comments []models.Comment 141 152
+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.Active.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.Active.Did { 611 - l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.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 perferom 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.Active.Did { 803 - l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.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
··· 1209 1209 type IssueCommentBodyParams struct { 1210 1210 LoggedInUser *oauth.MultiAccountUser 1211 1211 RepoInfo repoinfo.RepoInfo 1212 - Issue *models.Issue 1213 1212 Comment *models.Comment 1214 1213 } 1215 1214 ··· 1594 1593 return p.execute("timeline/home", w, params) 1595 1594 } 1596 1595 1596 + type CommentBodyFragmentParams struct { 1597 + Comment models.Comment 1598 + } 1599 + 1600 + func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error { 1601 + return p.executePlain("fragments/comment/commentBody", w, params) 1602 + } 1603 + 1604 + type EditCommentFragmentParams struct { 1605 + Comment models.Comment 1606 + } 1607 + 1608 + func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error { 1609 + return p.executePlain("fragments/comment/edit", w, params) 1610 + } 1611 + 1612 + type ReplyCommentFragmentParams struct { 1613 + LoggedInUser *oauth.MultiAccountUser 1614 + } 1615 + 1616 + func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error { 1617 + return p.executePlain("fragments/comment/reply", w, params) 1618 + } 1619 + 1620 + type ReplyPlaceholderFragmentParams struct { 1621 + LoggedInUser *oauth.MultiAccountUser 1622 + } 1623 + 1624 + func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error { 1625 + return p.executePlain("fragments/comment/replyPlaceholder", w, params) 1626 + } 1627 + 1597 1628 func (p *Pages) Static() http.Handler { 1598 1629 if p.dev { 1599 1630 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" }} 46 48 </a> 47 49 {{ end }} 48 50 49 - {{ define "deleteIssueComment" }} 51 + {{ define "deleteCommentBtn" }} 50 52 <a 51 53 class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 52 - hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/" 54 + hx-delete="/comment?aturi={{ .Comment.AtUri }}" 53 55 hx-confirm="Are you sure you want to delete your comment?" 54 - hx-swap="outerHTML" 55 - hx-target="#comment-body-{{.Comment.Id}}" 56 - > 56 + > 57 57 {{ i "trash-2" "size-3" }} 58 58 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 59 59 </a>
+17 -12
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 18 </div> 12 19 {{ end }} ··· 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 }}
-34
appview/pages/templates/repo/issues/fragments/issueCommentActions.html
··· 1 - {{ define "repo/issues/fragments/issueCommentActions" }} 2 - {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 3 - {{ if and $isCommentOwner (not .Comment.Deleted) }} 4 - <div class="flex flex-wrap items-center gap-4 text-gray-500 dark:text-gray-400 text-sm pt-2"> 5 - {{ template "edit" . }} 6 - {{ template "delete" . }} 7 - </div> 8 - {{ end }} 9 - {{ end }} 10 - 11 - {{ define "edit" }} 12 - <a 13 - class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 14 - hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/edit" 15 - hx-swap="outerHTML" 16 - hx-target="#comment-body-{{.Comment.Id}}"> 17 - {{ i "pencil" "size-3" }} 18 - edit 19 - </a> 20 - {{ end }} 21 - 22 - {{ define "delete" }} 23 - <a 24 - class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 25 - hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/" 26 - hx-confirm="Are you sure you want to delete your comment?" 27 - hx-swap="outerHTML" 28 - hx-target="#comment-body-{{.Comment.Id}}" 29 - > 30 - {{ i "trash-2" "size-3" }} 31 - delete 32 - {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 33 - </a> 34 - {{ 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-indicator="#comment-button" 8 - hx-disabled-elt="#comment-form button" 9 - hx-on::after-request="if(event.detail.successful) this.reset()" 4 + hx-post="/comment" 5 + hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 6 + hx-indicator="find button[type='submit']" 7 + hx-disabled-elt="find button[type='submit']" 8 + hx-on::after-request="if(event.detail.successful) this.reset()" 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 ··· 53 54 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 54 55 </span> 55 56 </button> 56 - <div 57 - id="close-with-comment" 58 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 59 - hx-trigger="click from:#close-button" 60 - hx-disabled-elt="#close-with-comment" 61 - hx-target="#issue-comment" 62 - hx-indicator="#close-spinner" 63 - hx-vals="js:{body: document.getElementById('comment-textarea').value.trim() !== '' ? document.getElementById('comment-textarea').value : ''}" 64 - hx-swap="none" 65 - > 66 - </div> 67 57 <div 68 58 id="close-issue" 69 59 hx-disabled-elt="#close-issue" ··· 74 64 hx-swap="none" 75 65 > 76 66 </div> 77 - <script> 78 - document.addEventListener('htmx:configRequest', function(evt) { 79 - if (evt.target.id === 'close-with-comment') { 80 - const commentText = document.getElementById('comment-textarea').value.trim(); 81 - if (commentText === '') { 82 - evt.detail.parameters = {}; 83 - evt.preventDefault(); 84 - } 85 - } 86 - }); 87 - </script> 88 67 {{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (not .Issue.Open) }} 89 68 <button 90 69 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 629 646 target:ring-1 target:ring-gray-200 target:dark:ring-gray-700
+4 -124
appview/pulls/pulls.go
··· 279 279 diff = patchutil.Interdiff(previousPatch, currentPatch) 280 280 } 281 281 282 - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 282 + err = s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 283 283 LoggedInUser: user, 284 284 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 285 285 Pull: pull, ··· 300 300 301 301 LabelDefs: defs, 302 302 }) 303 + if err != nil { 304 + s.logger.Error("failed to render", "err", err) 305 + } 303 306 } 304 307 305 308 func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { ··· 758 761 759 762 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 760 763 user := s.oauth.GetMultiAccountUser(r) 761 - f, err := s.repoResolver.Resolve(r) 762 - if err != nil { 763 - s.logger.Error("failed to get repo and knot", "err", err) 764 - return 765 - } 766 764 767 765 pull, ok := r.Context().Value("pull").(*models.Pull) 768 766 if !ok { ··· 788 786 RoundNumber: roundNumber, 789 787 }) 790 788 return 791 - case http.MethodPost: 792 - body := r.FormValue("body") 793 - if body == "" { 794 - s.pages.Notice(w, "pull-comment", "Comment body is required") 795 - return 796 - } 797 - 798 - // TODO(boltless): normalize markdown body 799 - normalizedBody := body 800 - mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 801 - 802 - markdownBody := tangled.MarkupMarkdown{ 803 - Text: normalizedBody, 804 - Original: &body, 805 - Blobs: nil, 806 - } 807 - 808 - // ingest CID of PR record on-demand. 809 - // TODO(boltless): appview should ingest CID of atproto records 810 - cid, err := func() (syntax.CID, error) { 811 - ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 812 - if err != nil { 813 - return "", err 814 - } 815 - 816 - xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()} 817 - out, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoPullNSID, pull.OwnerDid, pull.Rkey) 818 - if err != nil { 819 - return "", err 820 - } 821 - if out.Cid == nil { 822 - return "", fmt.Errorf("record CID is empty") 823 - } 824 - 825 - cid, err := syntax.ParseCID(*out.Cid) 826 - if err != nil { 827 - return "", err 828 - } 829 - 830 - return cid, nil 831 - }() 832 - if err != nil { 833 - s.logger.Error("failed to backfill subject PR record", "err", err) 834 - s.pages.Notice(w, "pull-comment", "failed to backfill subject record") 835 - return 836 - } 837 - pullStrongRef := comatproto.RepoStrongRef{ 838 - Uri: pull.AtUri().String(), 839 - Cid: cid.String(), 840 - } 841 - 842 - comment := models.Comment{ 843 - Did: syntax.DID(user.Active.Did), 844 - Collection: tangled.FeedCommentNSID, 845 - Rkey: syntax.RecordKey(tid.TID()), 846 - 847 - Subject: pullStrongRef, 848 - Body: markdownBody, 849 - Created: time.Now(), 850 - ReplyTo: nil, 851 - PullRoundIdx: &roundNumber, 852 - } 853 - if err = comment.Validate(); err != nil { 854 - s.logger.Error("failed to validate comment", "err", err) 855 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 856 - return 857 - } 858 - 859 - client, err := s.oauth.AuthorizedClient(r) 860 - if err != nil { 861 - s.logger.Error("failed to get authorized client", "err", err) 862 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 863 - return 864 - } 865 - 866 - out, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 867 - Collection: comment.Collection.String(), 868 - Repo: comment.Did.String(), 869 - Rkey: comment.Rkey.String(), 870 - Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()}, 871 - }) 872 - if err != nil { 873 - s.logger.Error("failed to create pull comment", "err", err) 874 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 875 - return 876 - } 877 - 878 - comment.Cid = syntax.CID(out.Cid) 879 - 880 - // Start a transaction 881 - tx, err := s.db.BeginTx(r.Context(), nil) 882 - if err != nil { 883 - s.logger.Error("failed to start transaction", "err", err) 884 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 885 - return 886 - } 887 - defer tx.Rollback() 888 - 889 - // Create the pull comment in the database 890 - err = db.PutComment(tx, &comment, references) 891 - if err != nil { 892 - s.logger.Error("failed to create pull comment", "err", err) 893 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 894 - return 895 - } 896 - 897 - // Commit the transaction 898 - if err = tx.Commit(); err != nil { 899 - s.logger.Error("failed to commit transaction", "err", err) 900 - s.pages.Notice(w, "pull-comment", "Failed to create comment.") 901 - return 902 - } 903 - 904 - s.notifier.NewComment(r.Context(), &comment, mentions) 905 - 906 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 907 - s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, comment.Id)) 908 - return 909 789 } 910 790 } 911 791
+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) {
+393
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 + mentions, 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.Active.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 + s.notifier.NewComment(ctx, &comment, mentions) 243 + 244 + // TODO: return comment or reply-comment fragment 245 + // onattach, htmx-callback to focus on comment. 246 + s.pages.HxRefresh(w) 247 + } 248 + 249 + func (s *State) EditComment(w http.ResponseWriter, r *http.Request) { 250 + l := s.logger.With("handler", "EditComment") 251 + user := s.oauth.GetMultiAccountUser(r) 252 + 253 + noticeId := "comment-error" 254 + ctx := r.Context() 255 + 256 + commentAt := r.FormValue("aturi") 257 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 258 + if err != nil { 259 + l.Error("failed to fetch comment", "aturi", commentAt, "err", err) 260 + s.pages.Notice(w, noticeId, "Failed to fetch comment") 261 + return 262 + } 263 + 264 + if comment.Did.String() != user.Active.Did { 265 + l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 266 + s.pages.Notice(w, noticeId, "You are not the author of this comment") 267 + return 268 + } 269 + 270 + body := r.FormValue("body") 271 + if body == "" { 272 + s.pages.Notice(w, noticeId, "Body is required") 273 + return 274 + } 275 + 276 + // TODO(boltless): normalize markdown body 277 + normalizedBody := body 278 + _, references := s.mentionsResolver.Resolve(ctx, body) 279 + 280 + now := time.Now() 281 + newComment := comment 282 + newComment.Body = tangled.MarkupMarkdown{ 283 + Text: normalizedBody, 284 + Original: &body, 285 + Blobs: nil, 286 + } 287 + newComment.Edited = &now 288 + 289 + client, err := s.oauth.AuthorizedClient(r) 290 + if err != nil { 291 + l.Error("failed to get authorized client", "err", err) 292 + s.pages.Notice(w, noticeId, "Failed to create comment. try again later.") 293 + return 294 + } 295 + 296 + // update the record first 297 + exCid := comment.Cid.String() 298 + out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 299 + Collection: newComment.Collection.String(), 300 + Repo: newComment.Did.String(), 301 + Rkey: newComment.Rkey.String(), 302 + SwapRecord: &exCid, 303 + Record: &lexutil.LexiconTypeDecoder{ 304 + Val: newComment.AsRecord(), 305 + }, 306 + }) 307 + if err != nil { 308 + l.Error("failed to update comment", "err", err) 309 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 310 + return 311 + } 312 + 313 + newComment.Cid = syntax.CID(out.Cid) 314 + 315 + tx, err := s.db.Begin() 316 + if err != nil { 317 + l.Error("failed to start transaction", "err", err) 318 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 319 + return 320 + } 321 + defer tx.Rollback() 322 + 323 + err = db.PutComment(tx, &newComment, references) 324 + if err != nil { 325 + l.Error("failed to perferom update-description query", "err", err) 326 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 327 + return 328 + } 329 + err = tx.Commit() 330 + if err != nil { 331 + l.Error("failed to commit transaction", "err", err) 332 + s.pages.Notice(w, noticeId, "Failed to update comment, try again later.") 333 + return 334 + } 335 + 336 + // TODO: return full comment fragment so we can update comment header too 337 + s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{ 338 + Comment: newComment, 339 + }) 340 + } 341 + 342 + func (s *State) DeleteComment(w http.ResponseWriter, r *http.Request) { 343 + l := s.logger.With("handler", "DeleteComment") 344 + user := s.oauth.GetMultiAccountUser(r) 345 + 346 + noticeId := "comment" 347 + ctx := r.Context() 348 + 349 + commentAt := r.URL.Query().Get("aturi") 350 + comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt)) 351 + if err != nil { 352 + l.Error("failed to fetch comment", "aturi", commentAt) 353 + s.pages.Notice(w, noticeId, "Failed to fetch comment.") 354 + return 355 + } 356 + 357 + if comment.Did.String() != user.Active.Did { 358 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 359 + s.pages.Notice(w, noticeId, "you are not the author of this comment") 360 + return 361 + } 362 + 363 + if comment.Deleted != nil { 364 + s.pages.Notice(w, noticeId, "Comment already deleted") 365 + return 366 + } 367 + 368 + client, err := s.oauth.AuthorizedClient(r) 369 + if err != nil { 370 + l.Error("failed to get authorized client", "err", err) 371 + s.pages.Notice(w, "comment", "Failed to delete comment.") 372 + return 373 + } 374 + _, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 375 + Collection: comment.Collection.String(), 376 + Repo: comment.Did.String(), 377 + Rkey: comment.Rkey.String(), 378 + }) 379 + if err != nil { 380 + l.Error("failed to delete from PDS", "err", err) 381 + s.pages.Notice(w, noticeId, "Failed to delete comment, try again later.") 382 + return 383 + } 384 + 385 + // optimistic update for htmx response 386 + now := time.Now() 387 + comment.Body = tangled.MarkupMarkdown{} 388 + comment.Deleted = &now 389 + 390 + s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{ 391 + Comment: comment, 392 + }) 393 + }
+10
appview/state/router.go
··· 185 185 r.Delete("/", s.React) 186 186 }) 187 187 188 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 189 + r.Get("/", s.CommentBodyFragment) 190 + r.Get("/edit", s.EditCommentFragment) 191 + r.Get("/reply", s.NewReplyCommentFragment) 192 + r.Get("/reply/placeholder", s.ReplyPlaceholderFragment) 193 + r.Post("/", s.NewComment) 194 + r.Patch("/", s.EditComment) 195 + r.Delete("/", s.DeleteComment) 196 + }) 197 + 188 198 r.Route("/profile", func(r chi.Router) { 189 199 r.Use(middleware.AuthMiddleware(s.oauth)) 190 200 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.Active.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