diff --git a/appview/db/comments.go b/appview/db/comments.go
index 1a9a1504..9b065de3 100644
--- a/appview/db/comments.go
+++ b/appview/db/comments.go
@@ -136,6 +136,17 @@ func DeleteComments(e Execer, filters ...orm.Filter) error {
return err
}
+func GetComment(e Execer, filters ...orm.Filter) (models.Comment, error) {
+ comments, err := GetComments(e, filters...)
+ if err != nil {
+ return models.Comment{}, err
+ }
+ if len(comments) != 1 {
+ return models.Comment{}, fmt.Errorf("expected 1 comment, got %d", len(comments))
+ }
+ return comments[0], nil
+}
+
func GetComments(e Execer, filters ...orm.Filter) ([]models.Comment, error) {
var comments []models.Comment
diff --git a/appview/issues/issues.go b/appview/issues/issues.go
index 9e9757d4..3a2094f9 100644
--- a/appview/issues/issues.go
+++ b/appview/issues/issues.go
@@ -133,7 +133,7 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
defs[l.AtUri().String()] = &l
}
- rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
+ err = rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
Issue: issue,
@@ -143,6 +143,9 @@ func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
UserReacted: userReactions,
LabelDefs: defs,
})
+ if err != nil {
+ l.Error("failed to render", "err", err)
+ }
}
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
@@ -547,13 +550,6 @@ func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
l := rp.logger.With("handler", "IssueComment")
user := rp.oauth.GetMultiAccountUser(r)
- issue, ok := r.Context().Value("issue").(*models.Issue)
- if !ok {
- l.Error("failed to get issue")
- rp.pages.Error404(w)
- return
- }
-
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetComments(
rp.db,
@@ -574,7 +570,6 @@ func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
LoggedInUser: user,
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
- Issue: issue,
Comment: &comment,
})
}
@@ -693,7 +688,6 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
LoggedInUser: user,
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
- Issue: issue,
Comment: &newComment,
})
}
@@ -775,13 +769,6 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
l := rp.logger.With("handler", "DeleteIssueComment")
user := rp.oauth.GetMultiAccountUser(r)
- issue, ok := r.Context().Value("issue").(*models.Issue)
- if !ok {
- l.Error("failed to get issue")
- rp.pages.Error404(w)
- return
- }
-
commentId := chi.URLParam(r, "commentId")
comments, err := db.GetComments(
rp.db,
@@ -845,7 +832,6 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
LoggedInUser: user,
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
- Issue: issue,
Comment: &comment,
})
}
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 02ef4008..4b428d08 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -1208,7 +1208,6 @@ func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentP
type IssueCommentBodyParams struct {
LoggedInUser *oauth.MultiAccountUser
RepoInfo repoinfo.RepoInfo
- Issue *models.Issue
Comment *models.Comment
}
@@ -1593,6 +1592,40 @@ func (p *Pages) Home(w io.Writer, params TimelineParams) error {
return p.execute("timeline/home", w, params)
}
+type CommentBodyFragmentParams struct {
+ Comment models.Comment
+}
+
+func (p *Pages) CommentBodyFragment(w io.Writer, params CommentBodyFragmentParams) error {
+ return p.executePlain("fragments/comment/commentBody", w, params)
+}
+
+type EditCommentFragmentParams struct {
+ Comment models.Comment
+}
+
+func (p *Pages) EditCommentFragment(w io.Writer, params EditCommentFragmentParams) error {
+ return p.executePlain("fragments/comment/edit", w, params)
+}
+
+type ReplyCommentFragmentParams struct {
+ LoggedInUser *oauth.MultiAccountUser
+ Comment models.Comment
+}
+
+func (p *Pages) ReplyCommentFragment(w io.Writer, params ReplyCommentFragmentParams) error {
+ return p.executePlain("fragments/comment/reply", w, params)
+}
+
+type ReplyPlaceholderFragmentParams struct {
+ LoggedInUser *oauth.MultiAccountUser
+ Comment struct{ AtUri string }
+}
+
+func (p *Pages) ReplyPlaceholderFragment(w io.Writer, params ReplyPlaceholderFragmentParams) error {
+ return p.executePlain("fragments/comment/replyPlaceholder", w, params)
+}
+
func (p *Pages) Static() http.Handler {
if p.dev {
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
diff --git a/appview/pages/templates/fragments/comment/commentBody.html b/appview/pages/templates/fragments/comment/commentBody.html
new file mode 100644
index 00000000..9bef885e
--- /dev/null
+++ b/appview/pages/templates/fragments/comment/commentBody.html
@@ -0,0 +1,9 @@
+{{ define "fragments/comment/commentBody" }}
+
+ {{ if not .Comment.Deleted }}
+
{{ .Comment.Body.Text | markdown }}
+ {{ else }}
+
[deleted by author]
+ {{ end }}
+
+{{ end }}
diff --git a/appview/pages/templates/fragments/comment/commentHeader.html b/appview/pages/templates/fragments/comment/commentHeader.html
new file mode 100644
index 00000000..d3a29312
--- /dev/null
+++ b/appview/pages/templates/fragments/comment/commentHeader.html
@@ -0,0 +1,60 @@
+{{ define "fragments/comment/commentHeader" }}
+
+ {{ $handle := resolve .Comment.Did.String }}
+
{{ $handle }}
+ {{ template "hats" $ }}
+
+ {{ template "timestamp" . }}
+ {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did.String) }}
+ {{ if and $isCommentOwner (not .Comment.Deleted) }}
+ {{ if not .Comment.IsLegacy }}
+ {{ template "editCommentBtn" . }}
+ {{ end }}
+ {{ template "deleteCommentBtn" . }}
+ {{ end }}
+
+{{ end }}
+
+{{ define "hats" }}
+ {{ $isIssueAuthor := eq .Comment.Did .Issue.Did }}
+ {{ if $isIssueAuthor }}
+ (author)
+ {{ end }}
+{{ end }}
+
+{{ define "timestamp" }}
+
+{{ end }}
+
+{{ define "editCommentBtn" }}
+
+ {{ i "pencil" "size-3" }}
+
+{{ end }}
+
+{{ define "deleteCommentBtn" }}
+
+ {{ i "trash-2" "size-3" }}
+ {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }}
+
+{{ end }}
diff --git a/appview/pages/templates/fragments/comment/edit.html b/appview/pages/templates/fragments/comment/edit.html
new file mode 100644
index 00000000..8b39dfee
--- /dev/null
+++ b/appview/pages/templates/fragments/comment/edit.html
@@ -0,0 +1,44 @@
+{{ define "fragments/comment/edit" }}
+
+
+
+ {{ template "editActions" $ }}
+
+{{ end }}
+
+{{ define "editActions" }}
+
+ {{ template "cancel" . }}
+ {{ template "save" . }}
+
+{{ end }}
+
+{{ define "save" }}
+
+{{ end }}
+
+{{ define "cancel" }}
+
+{{ end }}
diff --git a/appview/pages/templates/fragments/comment/reply.html b/appview/pages/templates/fragments/comment/reply.html
new file mode 100644
index 00000000..c7929861
--- /dev/null
+++ b/appview/pages/templates/fragments/comment/reply.html
@@ -0,0 +1,66 @@
+{{ define "fragments/comment/reply" }}
+
+{{ end }}
+
+{{ define "replyActions" }}
+
+ {{ template "cancel" . }}
+ {{ template "reply" . }}
+
+{{ end }}
+
+{{ define "cancel" }}
+
+{{ end }}
+
+{{ define "reply" }}
+
+{{ end }}
diff --git a/appview/pages/templates/fragments/comment/replyPlaceholder.html b/appview/pages/templates/fragments/comment/replyPlaceholder.html
new file mode 100644
index 00000000..2c9ecf0a
--- /dev/null
+++ b/appview/pages/templates/fragments/comment/replyPlaceholder.html
@@ -0,0 +1,14 @@
+{{ define "fragments/comment/replyPlaceholder" }}
+ {{ if .LoggedInUser }}
+ {{ template "user/fragments/pic" (list .LoggedInUser.Did "size-8 mr-1") }}
+ {{ end }}
+
+
+{{ end }}
diff --git a/appview/pages/templates/repo/issues/fragments/commentList.html b/appview/pages/templates/repo/issues/fragments/commentList.html
index 4fd6cd91..4c6a7a78 100644
--- a/appview/pages/templates/repo/issues/fragments/commentList.html
+++ b/appview/pages/templates/repo/issues/fragments/commentList.html
@@ -1,40 +1,44 @@
{{ define "repo/issues/fragments/commentList" }}
{{ range $item := .CommentList }}
- {{ template "commentListing" (list $ .) }}
+ {{ template "commentListItem" (list $ .) }}
{{ end }}
{{ end }}
-{{ define "commentListing" }}
+{{ define "commentListItem" }}
{{ $root := index . 0 }}
- {{ $comment := index . 1 }}
+ {{ $item := index . 1 }}
{{ $params :=
(dict
- "RepoInfo" $root.RepoInfo
"LoggedInUser" $root.LoggedInUser
- "Issue" $root.Issue
- "Comment" $comment.Self) }}
+ "Comment" $item.Self) }}
{{ template "topLevelComment" $params }}
- {{ range $index, $reply := $comment.Replies }}
+ {{ range $index, $reply := $item.Replies }}
{{
template "replyComment"
(dict
- "RepoInfo" $root.RepoInfo
"LoggedInUser" $root.LoggedInUser
- "Issue" $root.Issue
- "Comment" $reply)
+ "Comment" $reply)
}}
{{ end }}
- {{ template "repo/issues/fragments/replyIssueCommentPlaceholder" $params }}
+
+ {{ if $item.Self.IsLegacy }}
+ {{ if $root.LoggedInUser }}
+ Can't reply to legacy comment.
+ {{ end }}
+ {{ else }}
+ {{ template "fragments/comment/replyPlaceholder" (dict "LoggedInUser" $root.LoggedInUser) }}
+ {{ end }}
+
{{ end }}
@@ -44,8 +48,8 @@
{{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }}
- {{ template "repo/issues/fragments/issueCommentHeader" . }}
- {{ template "repo/issues/fragments/issueCommentBody" . }}
+ {{ template "fragments/comment/commentHeader" . }}
+ {{ template "fragments/comment/commentBody" . }}
{{ end }}
@@ -56,8 +60,8 @@
{{ template "user/fragments/picLink" (list .Comment.Did.String "size-8 mr-1") }}
- {{ template "repo/issues/fragments/issueCommentHeader" . }}
- {{ template "repo/issues/fragments/issueCommentBody" . }}
+ {{ template "fragments/comment/commentHeader" . }}
+ {{ template "fragments/comment/commentBody" . }}
{{ end }}
diff --git a/appview/state/comment.go b/appview/state/comment.go
new file mode 100644
index 00000000..f3a68256
--- /dev/null
+++ b/appview/state/comment.go
@@ -0,0 +1,373 @@
+package state
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ comatproto "github.com/bluesky-social/indigo/api/atproto"
+ "github.com/bluesky-social/indigo/atproto/syntax"
+ lexutil "github.com/bluesky-social/indigo/lex/util"
+ indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
+ "tangled.org/core/api/tangled"
+ "tangled.org/core/appview/db"
+ "tangled.org/core/appview/models"
+ "tangled.org/core/appview/pages"
+ "tangled.org/core/orm"
+ "tangled.org/core/tid"
+)
+
+func (s *State) CommentBodyFragment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "CommentBodyFragment")
+
+ commentAt := r.URL.Query().Get("aturi")
+ comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
+ if err != nil {
+ l.Error("failed to fetch comment", "aturi", commentAt)
+ http.Error(w, "Failed to fetch comment", http.StatusInternalServerError)
+ return
+ }
+
+ s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
+ Comment: comment,
+ })
+}
+
+func (s *State) EditCommentFragment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "EditCommentFragment")
+
+ commentAt := r.URL.Query().Get("aturi")
+ comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
+ if err != nil {
+ l.Error("failed to fetch comment", "aturi", commentAt)
+ http.Error(w, "Failed to fetch comment", http.StatusInternalServerError)
+ return
+ }
+
+ s.pages.EditCommentFragment(w, pages.EditCommentFragmentParams{
+ Comment: comment,
+ })
+}
+
+func (s *State) NewReplyCommentFragment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "NewReplyCommentFragment")
+
+ parentAt := r.URL.Query().Get("parent")
+ parent, err := db.GetComment(s.db, orm.FilterEq("at_uri", parentAt))
+ if err != nil {
+ l.Error("failed to fetch comment", "aturi", parentAt)
+ http.Error(w, "Failed to fetch comment", http.StatusInternalServerError)
+ return
+ }
+
+ s.pages.ReplyCommentFragment(w, pages.ReplyCommentFragmentParams{
+ LoggedInUser: s.oauth.GetMultiAccountUser(r),
+ Comment: parent,
+ })
+}
+
+func (s *State) ReplyPlaceholderFragment(w http.ResponseWriter, r *http.Request) {
+ s.pages.ReplyPlaceholderFragment(w, pages.ReplyPlaceholderFragmentParams{
+ LoggedInUser: s.oauth.GetMultiAccountUser(r),
+ Comment: struct{ AtUri string }{
+ AtUri: r.URL.Query().Get("parent"),
+ },
+ })
+}
+
+func (s *State) NewComment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "NewComment")
+ user := s.oauth.GetMultiAccountUser(r)
+
+ noticeId := "comment"
+ ctx := r.Context()
+
+ body := r.FormValue("body")
+ if body == "" {
+ s.pages.Notice(w, noticeId, "Body is required")
+ return
+ }
+
+ // TODO(boltless): normalize markdown body
+ normalizedBody := body
+ mentions, references := s.mentionsResolver.Resolve(ctx, body)
+
+ markdownBody := tangled.Markup_Markdown{
+ Text: normalizedBody,
+ Original: &body,
+ Blobs: nil,
+ }
+
+ subjectUri, err := syntax.ParseATURI(r.FormValue("subject-uri"))
+ if err != nil {
+ s.pages.Notice(w, noticeId, "Subject URI should be valid AT-URI")
+ return
+ }
+
+ // ingest CID of subject record on-demand.
+ // TODO(boltless): appview should ingest CID of all atproto records
+ subjectCid, err := func(uri syntax.ATURI) (syntax.CID, error) {
+ ident, err := s.idResolver.ResolveIdent(ctx, uri.Authority().String())
+ if err != nil {
+ return "", err
+ }
+
+ xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()}
+ out, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", uri.Collection().String(), ident.DID.String(), uri.RecordKey().String())
+ if err != nil {
+ return "", err
+ }
+ if out.Cid == nil {
+ return "", fmt.Errorf("record CID is empty")
+ }
+
+ cid, err := syntax.ParseCID(*out.Cid)
+ if err != nil {
+ return "", err
+ }
+
+ return cid, nil
+ }(subjectUri)
+ if err != nil {
+ l.Error("failed to backfill subject record", "subject.uri", subjectUri, "err", err)
+ s.pages.Notice(w, noticeId, "failed to backfill subject record")
+ return
+ }
+
+ subject := comatproto.RepoStrongRef{
+ Uri: subjectUri.String(),
+ Cid: subjectCid.String(),
+ }
+
+ var replyTo *comatproto.RepoStrongRef
+ replyToUriRaw := r.FormValue("reply-to-uri")
+ replyToCidRaw := r.FormValue("reply-to-cid")
+ if replyToUriRaw != "" && replyToCidRaw != "" {
+ uri, err := syntax.ParseATURI(replyToUriRaw)
+ if err != nil {
+ s.pages.Notice(w, noticeId, "reply-to-uri should be valid AT-URI")
+ return
+ }
+ cid, err := syntax.ParseCID(replyToCidRaw)
+ if err != nil {
+ s.pages.Notice(w, noticeId, "reply-to-cid should be valid CID")
+ return
+ }
+ replyTo = &comatproto.RepoStrongRef{
+ Uri: uri.String(),
+ Cid: cid.String(),
+ }
+ }
+
+ comment := models.Comment{
+ Did: syntax.DID(user.Active.Did),
+ Collection: tangled.FeedCommentNSID,
+ Rkey: syntax.RecordKey(tid.TID()),
+
+ Subject: subject,
+ Body: markdownBody,
+ Created: time.Now(),
+ ReplyTo: replyTo,
+ }
+ if err = comment.Validate(); err != nil {
+ l.Error("failed to validate comment", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment.")
+ return
+ }
+
+ client, err := s.oauth.AuthorizedClient(r)
+ if err != nil {
+ l.Error("failed to get authorized client", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment.")
+ return
+ }
+
+ // create a record first
+ out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
+ Collection: comment.Collection.String(),
+ Repo: comment.Did.String(),
+ Rkey: comment.Rkey.String(),
+ Record: &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()},
+ })
+ if err != nil {
+ l.Error("failed to create comment", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment.")
+ return
+ }
+
+ comment.Cid = syntax.CID(out.Cid)
+
+ tx, err := s.db.Begin()
+ if err != nil {
+ l.Error("failed to start transaction", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment, try again later.")
+ return
+ }
+ defer tx.Rollback()
+
+ err = db.PutComment(tx, &comment, references)
+ if err != nil {
+ l.Error("failed to create comment", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment.")
+ return
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ l.Error("failed to commit transaction", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment, try again later.")
+ return
+ }
+
+ s.notifier.NewComment(ctx, &comment, mentions)
+
+ // TODO: return comment or reply-comment fragment
+ // onattach, htmx-callback to focus on comment.
+ panic("unimplemented")
+}
+
+func (s *State) EditComment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "EditComment")
+ user := s.oauth.GetMultiAccountUser(r)
+
+ noticeId := "comment"
+ ctx := r.Context()
+
+ commentAt := r.URL.Query().Get("aturi")
+ comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
+ if err != nil {
+ l.Error("failed to fetch comment", "aturi", commentAt)
+ s.pages.Notice(w, noticeId, "Failed to fetch comment")
+ return
+ }
+
+ if comment.Did.String() != user.Active.Did {
+ l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did)
+ s.pages.Notice(w, noticeId, "You are not the author of this comment")
+ return
+ }
+
+ body := r.FormValue("body")
+ if body == "" {
+ s.pages.Notice(w, noticeId, "Body is required")
+ return
+ }
+
+ // TODO(boltless): normalize markdown body
+ normalizedBody := body
+ _, references := s.mentionsResolver.Resolve(ctx, body)
+
+ now := time.Now()
+ newComment := comment
+ newComment.Body = tangled.Markup_Markdown{
+ Text: normalizedBody,
+ Original: &body,
+ Blobs: nil,
+ }
+ newComment.Edited = &now
+
+ client, err := s.oauth.AuthorizedClient(r)
+ if err != nil {
+ l.Error("failed to get authorized client", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to create comment. try again later.")
+ return
+ }
+
+ // update the record first
+ exCid := comment.Cid.String()
+ out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
+ Collection: newComment.Collection.String(),
+ Repo: newComment.Did.String(),
+ Rkey: newComment.Rkey.String(),
+ SwapRecord: &exCid,
+ Record: &lexutil.LexiconTypeDecoder{
+ Val: newComment.AsRecord(),
+ },
+ })
+ if err != nil {
+ l.Error("failed to update comment", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
+ return
+ }
+
+ newComment.Cid = syntax.CID(out.Cid)
+
+ tx, err := s.db.Begin()
+ if err != nil {
+ l.Error("failed to start transaction", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
+ return
+ }
+ defer tx.Rollback()
+
+ err = db.PutComment(tx, &newComment, references)
+ if err != nil {
+ l.Error("failed to perferom update-description query", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
+ return
+ }
+ err = tx.Commit()
+ if err != nil {
+ l.Error("failed to commit transaction", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
+ return
+ }
+
+ s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
+ Comment: comment,
+ })
+}
+
+func (s *State) DeleteComment(w http.ResponseWriter, r *http.Request) {
+ l := s.logger.With("handler", "DeleteComment")
+ user := s.oauth.GetMultiAccountUser(r)
+
+ noticeId := "comment"
+ ctx := r.Context()
+
+ commentAt := r.URL.Query().Get("aturi")
+ comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
+ if err != nil {
+ l.Error("failed to fetch comment", "aturi", commentAt)
+ s.pages.Notice(w, noticeId, "Failed to fetch comment.")
+ return
+ }
+
+ if comment.Did.String() != user.Active.Did {
+ l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did)
+ s.pages.Notice(w, noticeId, "you are not the author of this comment")
+ return
+ }
+
+ if comment.Deleted != nil {
+ s.pages.Notice(w, noticeId, "Comment already deleted")
+ return
+ }
+
+ client, err := s.oauth.AuthorizedClient(r)
+ if err != nil {
+ l.Error("failed to get authorized client", "err", err)
+ s.pages.Notice(w, "comment", "Failed to delete comment.")
+ return
+ }
+ _, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
+ Collection: comment.Collection.String(),
+ Repo: comment.Did.String(),
+ Rkey: comment.Rkey.String(),
+ })
+ if err != nil {
+ l.Error("failed to delete from PDS", "err", err)
+ s.pages.Notice(w, noticeId, "Failed to delete comment, try again later.")
+ return
+ }
+
+ // optimistic update for htmx response
+ now := time.Now()
+ comment.Body = tangled.Markup_Markdown{}
+ comment.Deleted = &now
+
+ s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
+ Comment: comment,
+ })
+}
diff --git a/appview/state/router.go b/appview/state/router.go
index a76271ac..541542dc 100644
--- a/appview/state/router.go
+++ b/appview/state/router.go
@@ -161,6 +161,16 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler {
r.Delete("/", s.React)
})
+ r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
+ r.Get("/", s.CommentBodyFragment)
+ r.Get("/edit", s.EditCommentFragment)
+ r.Get("/reply", s.NewReplyCommentFragment)
+ r.Get("/reply/placeholder", s.ReplyPlaceholderFragment)
+ r.Post("/", s.NewComment)
+ r.Put("/", s.EditComment)
+ r.Delete("/", s.DeleteComment)
+ })
+
r.Route("/profile", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.oauth))
r.Get("/edit-bio", s.EditBioFragment)