diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index d9e235fc..e6b6bf19 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -662,6 +662,14 @@ async fn patch_update_handler( patch::Action::ReviewEdit { review, summary } => { patch.edit_review(review, summary, &signer)?; } + patch::Action::ReviewComment { + review, + body, + reply_to, + .. + } => { + patch.review_comment(review, body, None, reply_to, &signer)?; + } patch::Action::ReviewCommentEdit { review, comment, @@ -669,6 +677,17 @@ async fn patch_update_handler( } => { patch.edit_review_comment(review, comment, body, &signer)?; } + patch::Action::ReviewCommentReact { + review, + comment, + reaction, + active, + } => { + patch.react_review_comment(review, comment, reaction, active, &signer)?; + } + patch::Action::ReviewCommentRedact { review, comment } => { + patch.redact_review_comment(review, comment, &signer)?; + } patch::Action::Label { labels } => { patch.label(labels, &signer)?; } @@ -702,6 +721,24 @@ async fn patch_update_handler( } => { patch.comment(revision, body, reply_to, &signer)?; } + patch::Action::RevisionCommentEdit { + revision, + comment, + body, + } => { + patch.comment_edit(revision, comment, body, &signer)?; + } + patch::Action::RevisionCommentReact { + revision, + comment, + reaction, + active, + } => { + patch.comment_react(revision, comment, reaction, active, &signer)?; + } + patch::Action::RevisionCommentRedact { revision, comment } => { + patch.comment_redact(revision, comment, &signer)?; + } _ => { todo!(); } @@ -2315,7 +2352,38 @@ mod routes { .await; assert_eq!(response.status(), StatusCode::OK); + let comment_react_body = serde_json::to_vec(&json!({ + "type": "revision.comment.react", + "revision": CONTRIBUTOR_PATCH_ID, + "comment": CONTRIBUTOR_COMMENT_1, + "reaction": "🚀", + "active": true + })) + .unwrap(); + patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(comment_react_body)), + Some(SESSION_ID.to_string()), + ) + .await; + let comment_edit = serde_json::to_vec(&json!({ + "type": "revision.comment.edit", + "revision": CONTRIBUTOR_PATCH_ID, + "comment": CONTRIBUTOR_COMMENT_1, + "body": "EDIT: This is a root level comment" + })) + .unwrap(); + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(comment_edit)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); let reply_body = serde_json::to_vec(&json!({ "type": "revision.comment", "revision": CONTRIBUTOR_PATCH_ID, @@ -2355,10 +2423,10 @@ mod routes { "revisions": [ { "id": CONTRIBUTOR_PATCH_ID, - "description": "change `hello world` in README to something else", "author": { "id": CONTRIBUTOR_DID, }, + "description": "change `hello world` in README to something else", "base": PARENT, "oid": HEAD, "refs": [ @@ -2370,8 +2438,8 @@ mod routes { "author": { "id": CONTRIBUTOR_DID, }, - "body": "This is a root level comment", - "reactions": [], + "body": "EDIT: This is a root level comment", + "reactions": [["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8","🚀"]], "timestamp": TIMESTAMP, "replyTo": null, }, @@ -2417,6 +2485,51 @@ mod routes { assert_eq!(response.status(), StatusCode::OK); + let review_comment_body = serde_json::to_vec(&json!({ + "type": "review.comment", + "review": CONTRIBUTOR_PATCH_REVIEW, + "body": "This is a comment on a review" + })) + .unwrap(); + patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(review_comment_body)), + Some(SESSION_ID.to_string()), + ) + .await; + + let review_comment_edit_body = serde_json::to_vec(&json!({ + "type": "review.comment.edit", + "review": CONTRIBUTOR_PATCH_REVIEW, + "comment": CONTRIBUTOR_COMMENT_3, + "body": "EDIT: This is a comment on a review" + })) + .unwrap(); + patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(review_comment_edit_body)), + Some(SESSION_ID.to_string()), + ) + .await; + + let review_react_body = serde_json::to_vec(&json!({ + "type": "review.comment.react", + "review": CONTRIBUTOR_PATCH_REVIEW, + "comment": CONTRIBUTOR_COMMENT_3, + "reaction": "🚀", + "active": true + })) + .unwrap(); + patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(review_react_body)), + Some(SESSION_ID.to_string()), + ) + .await; + let response = get( &app, format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), @@ -2439,10 +2552,10 @@ mod routes { "revisions": [ { "id": CONTRIBUTOR_PATCH_ID, - "description": "change `hello world` in README to something else", "author": { "id": CONTRIBUTOR_DID, }, + "description": "change `hello world` in README to something else", "base": PARENT, "oid": HEAD, "refs": [ @@ -2457,7 +2570,19 @@ mod routes { }, "verdict": "accept", "summary": "A small review", - "comments": [], + "comments": [[ + "dd9743bb964ba22399548c86a3c1765020d58f48", + { + "author": "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "reactions": [ + [ + "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "🚀", + ], + ], + "body": "EDIT: This is a comment on a review", + }, + ]], "timestamp": TIMESTAMP, }, ], diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index 08e52112..689db3b6 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -43,8 +43,10 @@ pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1 pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; pub const CONTRIBUTOR_ISSUE_ID: &str = "7466975f0bef37b459887824a9655f3e78262522"; pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25"; +pub const CONTRIBUTOR_PATCH_REVIEW: &str = "ee3eeba95f4ec418b3d0714e18e0d1ff605dc0e6"; pub const CONTRIBUTOR_COMMENT_1: &str = "d0bb75b2c72ab8b5486d39f6cf5f41f104b63cb1"; -pub const CONTRIBUTOR_COMMENT_2: &str = "2a4ec5bcb1be09c1f2213f418c0159fff894b989"; +pub const CONTRIBUTOR_COMMENT_2: &str = "227947fa41420e363752357b2edd54d3ccadf7b3"; +pub const CONTRIBUTOR_COMMENT_3: &str = "dd9743bb964ba22399548c86a3c1765020d58f48"; /// Create a new profile. pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 8abb5b7f..a5237ca5 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -1160,6 +1160,45 @@ impl store::Transaction { }) } + /// Edit a comment on a patch revision. + pub fn comment_edit( + &mut self, + revision: RevisionId, + comment: CommentId, + body: S, + ) -> Result<(), store::Error> { + self.push(Action::RevisionCommentEdit { + revision, + comment, + body: body.to_string(), + }) + } + + /// React a comment on a patch revision. + pub fn comment_react( + &mut self, + revision: RevisionId, + comment: CommentId, + reaction: Reaction, + active: bool, + ) -> Result<(), store::Error> { + self.push(Action::RevisionCommentReact { + revision, + comment, + reaction, + active, + }) + } + + /// Redact a comment on a patch revision. + pub fn comment_redact( + &mut self, + revision: RevisionId, + comment: CommentId, + ) -> Result<(), store::Error> { + self.push(Action::RevisionCommentRedact { revision, comment }) + } + /// Comment on a review. pub fn review_comment( &mut self, @@ -1190,6 +1229,31 @@ impl store::Transaction { }) } + /// React to a review comment. + pub fn react_review_comment( + &mut self, + review: EntryId, + comment: EntryId, + reaction: Reaction, + active: bool, + ) -> Result<(), store::Error> { + self.push(Action::ReviewCommentReact { + review, + comment, + reaction, + active, + }) + } + + /// Redact a review comment. + pub fn redact_review_comment( + &mut self, + review: EntryId, + comment: EntryId, + ) -> Result<(), store::Error> { + self.push(Action::ReviewCommentRedact { review, comment }) + } + /// Review a patch revision. pub fn review( &mut self, @@ -1342,6 +1406,45 @@ where self.transaction("Comment", signer, |tx| tx.comment(revision, body, reply_to)) } + /// Edit a comment on a patch revision. + pub fn comment_edit( + &mut self, + revision: RevisionId, + comment: CommentId, + body: S, + signer: &G, + ) -> Result { + self.transaction("Edit comment", signer, |tx| { + tx.comment_edit(revision, comment, body) + }) + } + + /// React to a comment on a patch revision. + pub fn comment_react( + &mut self, + revision: RevisionId, + comment: CommentId, + reaction: Reaction, + active: bool, + signer: &G, + ) -> Result { + self.transaction("React comment", signer, |tx| { + tx.comment_react(revision, comment, reaction, active) + }) + } + + /// Redact a comment on a patch revision. + pub fn comment_redact( + &mut self, + revision: RevisionId, + comment: CommentId, + signer: &G, + ) -> Result { + self.transaction("Redact comment", signer, |tx| { + tx.comment_redact(revision, comment) + }) + } + /// Comment on a line of code as part of a review. pub fn review_comment( &mut self, @@ -1369,6 +1472,32 @@ where }) } + /// React to a review comment. + pub fn react_review_comment( + &mut self, + review: EntryId, + comment: EntryId, + reaction: Reaction, + active: bool, + signer: &G, + ) -> Result { + self.transaction("React review comment", signer, |tx| { + tx.react_review_comment(review, comment, reaction, active) + }) + } + + /// React to a review comment. + pub fn redact_review_comment( + &mut self, + review: EntryId, + comment: EntryId, + signer: &G, + ) -> Result { + self.transaction("React review comment", signer, |tx| { + tx.redact_review_comment(review, comment) + }) + } + /// Review a patch revision. pub fn review( &mut self, diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index bcdf7095..35bfd7a7 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -84,7 +84,11 @@ impl Serialize for Comment { } state.serialize_field("reactions", &self.reactions)?; state.serialize_field("body", self.body())?; - state.serialize_field("embeds", self.embeds())?; + + let embeds = self.embeds(); + if !embeds.is_empty() { + state.serialize_field("embeds", self.embeds())?; + } state.end() } }