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.
This commit is contained in:
Sebastian Martinez 2023-12-20 16:16:26 +01:00 committed by cloudhead
parent 3560e0066d
commit 5333f8217a
No known key found for this signature in database
5 changed files with 164 additions and 20 deletions

View File

@ -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::<Vec<_>>(),
"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::<Vec<_>>()
}).collect::<Vec<_>>(),
"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::<Vec<_>>(),
"embeds": comment.embeds().to_vec(),
"reactions": comment.reactions().collect::<Vec<_>>(),
"reactions": comment.reactions().iter().map(|(emoji, authors)|
json!({ "emoji": emoji, "authors": authors })
).collect::<Vec<_>>(),
"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::<Vec<_>>(),
"embeds": comment.embeds().to_vec(),
"reactions": comment.reactions().collect::<Vec<_>>(),
"reactions": comment.reactions().iter().map(|(emoji, authors)|
json!({ "emoji": emoji, "authors": authors })
).collect::<Vec<_>>(),
"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::<Vec<_>>(),
"embeds": comment.embeds().to_vec(),
"reactions": comment.reactions().collect::<Vec<_>>(),
"reactions": comment.reactions().iter().map(|(emoji, authors)|
json!({ "emoji": emoji, "authors": authors })
).collect::<Vec<_>>(),
"timestamp": comment.timestamp().as_secs(),
"replyTo": comment.reply_to(),
"location": comment.location(),

View File

@ -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": [

View File

@ -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
}

View File

@ -1332,6 +1332,10 @@ impl Revision {
&self.description.last().embeds
}
pub fn reactions(&self) -> &BTreeMap<Option<CodeLocation>, BTreeSet<(PublicKey, Reaction)>> {
&self.reactions
}
/// Author of the revision.
pub fn author(&self) -> &Author {
&self.author
@ -1593,6 +1597,22 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
})
}
/// React on a patch revision.
pub fn react(
&mut self,
revision: RevisionId,
reaction: Reaction,
location: Option<CodeLocation>,
active: bool,
) -> Result<(), store::Error> {
self.push(Action::RevisionReact {
revision,
reaction,
location,
active,
})
}
/// Comment on a patch revision.
pub fn comment<S: ToString>(
&mut self,
@ -1924,6 +1944,20 @@ where
})
}
/// React on a patch revision.
pub fn react<G: Signer>(
&mut self,
revision: RevisionId,
reaction: Reaction,
location: Option<CodeLocation>,
active: bool,
signer: &G,
) -> Result<EntryId, Error> {
self.transaction("React", signer, |tx| {
tx.react(revision, reaction, location, active)
})
}
/// Edit a comment on a patch revision.
pub fn comment_edit<G: Signer, S: ToString>(
&mut self,

View File

@ -196,8 +196,13 @@ impl<L> Comment<L> {
}
/// Comment reactions.
pub fn reactions(&self) -> impl Iterator<Item = (&ActorId, &Reaction)> {
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.