From 5333f8217aa535b89b834d342fdb54a841fe27eb Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 20 Dec 2023 16:16:26 +0100 Subject: [PATCH] httpd: Add reactions to revisions, improve json for comment reactions Instead of using tuples and lots of arrays, this commit tries to use `BTreeMap`s to provide more context to review, issues, patch and other comments. Also the returned object by the httpd is more aligned with what is needed by the consumer, so less recomputation is needed for display. --- radicle-httpd/src/api/json.rs | 24 +++++- radicle-httpd/src/api/v1/projects.rs | 112 ++++++++++++++++++++++++--- radicle/src/cob/issue.rs | 5 +- radicle/src/cob/patch.rs | 34 ++++++++ radicle/src/cob/thread.rs | 9 ++- 5 files changed, 164 insertions(+), 20 deletions(-) diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index 2d4d7d0f..562d71fd 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -1,10 +1,11 @@ //! Utilities for building JSON responses of our API. +use std::collections::BTreeMap; use std::path::Path; use std::str; use base64::prelude::{Engine, BASE64_STANDARD}; -use radicle::cob::CodeLocation; +use radicle::cob::{CodeLocation, Reaction}; use serde_json::{json, Value}; use radicle::cob::issue::{Issue, IssueId}; @@ -130,6 +131,15 @@ pub(crate) fn patch( "author": author(rev.author(), aliases.alias(rev.author().id())), "description": rev.description(), "edits": rev.edits().map(|e| edit(e, aliases)).collect::>(), + "reactions": rev.reactions().iter().flat_map(|(location, reaction)| { + let reactions = reaction.iter().fold(BTreeMap::new(), |mut acc: BTreeMap<&Reaction, Vec<_>>, (author, emoji)| { + acc.entry(emoji).or_default().push(author); + acc + }); + reactions.iter().map(|(emoji, authors)| + json!({"location": location, "emoji": emoji, "authors": authors }) + ).collect::>() + }).collect::>(), "base": rev.base(), "oid": rev.head(), "refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or_default(), @@ -191,7 +201,9 @@ fn issue_comment(id: &CommentId, comment: &Comment, aliases: &impl AliasStore) - "body": comment.body(), "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), "embeds": comment.embeds().to_vec(), - "reactions": comment.reactions().collect::>(), + "reactions": comment.reactions().iter().map(|(emoji, authors)| + json!({ "emoji": emoji, "authors": authors }) + ).collect::>(), "timestamp": comment.timestamp().as_secs(), "replyTo": comment.reply_to(), "resolved": comment.resolved(), @@ -210,7 +222,9 @@ fn patch_comment( "body": comment.body(), "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), "embeds": comment.embeds().to_vec(), - "reactions": comment.reactions().collect::>(), + "reactions": comment.reactions().iter().map(|(emoji, authors)| + json!({ "emoji": emoji, "authors": authors }) + ).collect::>(), "timestamp": comment.timestamp().as_secs(), "replyTo": comment.reply_to(), "location": comment.location(), @@ -230,7 +244,9 @@ fn review_comment( "body": comment.body(), "edits": comment.edits().map(|e| edit(e, aliases)).collect::>(), "embeds": comment.embeds().to_vec(), - "reactions": comment.reactions().collect::>(), + "reactions": comment.reactions().iter().map(|(emoji, authors)| + json!({ "emoji": emoji, "authors": authors }) + ).collect::>(), "timestamp": comment.timestamp().as_secs(), "replyTo": comment.reply_to(), "location": comment.location(), diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index f2734a06..35d40294 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -869,6 +869,12 @@ async fn patch_update_handler( patch.edit_revision(revision, description, embeds, &signer)? } patch::Action::RevisionRedact { revision } => patch.redact(revision, &signer)?, + patch::Action::RevisionReact { + revision, + reaction, + active, + location, + } => patch.react(revision, reaction, location, active, &signer)?, patch::Action::RevisionComment { revision, body, @@ -903,9 +909,6 @@ async fn patch_update_handler( patch::Action::RevisionCommentRedact { revision, comment } => { patch.comment_redact(revision, comment, &signer)? } - _ => { - todo!(); - } }; announce_refs(node, repo.id())?; @@ -2281,10 +2284,10 @@ mod routes { } ], "reactions": [ - [ - "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", - "🚀", - ], + { + "emoji": "🚀", + "authors": ["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"] + }, ], "timestamp": TIMESTAMP, "replyTo": null, @@ -2433,6 +2436,7 @@ mod routes { "revisions": [ { "id": CONTRIBUTOR_PATCH_ID, + "reactions": [], "author": { "id": CONTRIBUTOR_DID, }, @@ -2485,6 +2489,7 @@ mod routes { "revisions": [ { "id": CONTRIBUTOR_PATCH_ID, + "reactions": [], "author": { "id": CONTRIBUTOR_DID, }, @@ -2576,6 +2581,7 @@ mod routes { "revisions": [ { "id": CREATED_PATCH_ID, + "reactions": [], "author": { "id": CONTRIBUTOR_DID, }, @@ -2662,6 +2668,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -2736,6 +2743,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -2809,6 +2817,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -2834,6 +2843,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -2893,6 +2903,7 @@ mod routes { "revisions": [ { "id": CONTRIBUTOR_PATCH_ID, + "reactions": [], "author": { "id": CONTRIBUTOR_DID, }, @@ -2943,6 +2954,52 @@ mod routes { assert_eq!(response.status(), StatusCode::OK); + let body = serde_json::to_vec(&json!({ + "type": "revision.react", + "revision": CONTRIBUTOR_PATCH_ID, + "reaction": "🚀", + "location": { + "commit": INITIAL_COMMIT, + "path": "./README.md", + "new": { + "type": "lines", + "range": { + "start": 0, + "end": 1 + } + } + }, + "active": true, + })) + .unwrap(); + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + + let body = serde_json::to_vec(&json!({ + "type": "revision.react", + "revision": CONTRIBUTOR_PATCH_ID, + "reaction": "🙏", + "location": null, + "active": true, + })) + .unwrap(); + let response = patch( + &app, + format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), + Some(Body::from(body)), + Some(SESSION_ID.to_string()), + ) + .await; + + assert_eq!(response.status(), StatusCode::OK); + let response = get( &app, format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), @@ -2987,6 +3044,29 @@ mod routes { "embeds": [], }, ], + "reactions": [ + { + "location": null, + "emoji": "🙏", + "authors": ["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"], + }, + { + "location": { + "commit": INITIAL_COMMIT, + "path": "./README.md", + "old": null, + "new": { + "type": "lines", + "range": { + "start": 0, + "end": 1 + } + } + }, + "emoji": "🚀", + "authors": ["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"] + }, + ], "base": PARENT, "oid": HEAD, "refs": [ @@ -3123,6 +3203,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -3169,7 +3250,12 @@ mod routes { "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", } ], - "reactions": [["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8","🚀"]], + "reactions": [ + { + "emoji": "🚀", + "authors": ["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"] + }, + ], "timestamp": TIMESTAMP, "replyTo": null, "location": null, @@ -3350,6 +3436,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ @@ -3406,10 +3493,10 @@ mod routes { }, ], "reactions": [ - [ - "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", - "🚀", - ], + { + "emoji": "🚀", + "authors": ["z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"], + }, ], "timestamp": 1671125284, "replyTo": null, @@ -3506,6 +3593,7 @@ mod routes { "embeds": [], }, ], + "reactions": [], "base": PARENT, "oid": HEAD, "refs": [ diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index 5058fa84..e9b8d04b 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -1167,9 +1167,10 @@ mod test { let id = issue.id; let issue = issues.get(&id).unwrap().unwrap(); - let (_, r) = issue.comment(&comment).unwrap().reactions().next().unwrap(); + let reactions = issue.comment(&comment).unwrap().reactions(); + let authors = reactions.get(&reaction).unwrap(); - assert_eq!(r, &reaction); + assert_eq!(authors.first().unwrap(), &node.signer.public_key()); // TODO: Test multiple reactions from same author and different authors } diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index d6983af2..7972cd6c 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -1332,6 +1332,10 @@ impl Revision { &self.description.last().embeds } + pub fn reactions(&self) -> &BTreeMap, BTreeSet<(PublicKey, Reaction)>> { + &self.reactions + } + /// Author of the revision. pub fn author(&self) -> &Author { &self.author @@ -1593,6 +1597,22 @@ impl store::Transaction { }) } + /// React on a patch revision. + pub fn react( + &mut self, + revision: RevisionId, + reaction: Reaction, + location: Option, + active: bool, + ) -> Result<(), store::Error> { + self.push(Action::RevisionReact { + revision, + reaction, + location, + active, + }) + } + /// Comment on a patch revision. pub fn comment( &mut self, @@ -1924,6 +1944,20 @@ where }) } + /// React on a patch revision. + pub fn react( + &mut self, + revision: RevisionId, + reaction: Reaction, + location: Option, + active: bool, + signer: &G, + ) -> Result { + self.transaction("React", signer, |tx| { + tx.react(revision, reaction, location, active) + }) + } + /// Edit a comment on a patch revision. pub fn comment_edit( &mut self, diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index e71f6597..0ed50d4d 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -196,8 +196,13 @@ impl Comment { } /// Comment reactions. - pub fn reactions(&self) -> impl Iterator { - self.reactions.iter().map(|(a, r)| (a, r)) + pub fn reactions(&self) -> BTreeMap<&Reaction, Vec<&ActorId>> { + self.reactions + .iter() + .fold(BTreeMap::new(), |mut acc, (author, reaction)| { + acc.entry(reaction).or_default().push(author); + acc + }) } /// Get comment location, if any.