diff --git a/radicle-httpd/src/api/json.rs b/radicle-httpd/src/api/json.rs index 63f956a9..318afba3 100644 --- a/radicle-httpd/src/api/json.rs +++ b/radicle-httpd/src/api/json.rs @@ -242,6 +242,7 @@ struct Comment<'a> { id: CommentId, author: Value, body: &'a str, + edits: Vec, embeds: Vec>, reactions: Vec<(&'a ActorId, &'a Reaction)>, #[serde(with = "radicle::serde_ext::localtime::time")] @@ -257,6 +258,17 @@ impl<'a> Comment<'a> { id: *id, author: author(&comment_author, aliases.alias(comment_author.id())), body: comment.body(), + edits: comment + .edits() + .map(|edit| { + json!({ + "author": author(&Author::from(edit.author), aliases.alias(&edit.author)), + "body": edit.body, + "timestamp": edit.timestamp, + "embeds": edit.embeds, + }) + }) + .collect::>(), embeds: comment.embeds().to_vec(), reactions: comment.reactions().collect::>(), timestamp: comment.timestamp(), diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index e75b11d6..b1d56460 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -1995,6 +1995,16 @@ mod routes { "id": DID }, "body": "Change 'hello world' to 'hello everyone'", + "edits": [ + { + "author": { + "id": DID, + }, + "body": "Change 'hello world' to 'hello everyone'", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [], + }, + ], "embeds": [], "reactions": [], "timestamp": TIMESTAMP, @@ -2070,6 +2080,21 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "Change 'hello world' to 'hello everyone'", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "Change 'hello world' to 'hello everyone'", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [ + { + "name": "example.html", + "content": "git:b62df2ec90365e3749cd4fa431cb844492908b84", + }, + ], + }, + ], "embeds": [ { "name": "example.html", @@ -2203,6 +2228,29 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "EDIT: Change 'hello world' to 'hello anyone'", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "Change 'hello world' to 'hello everyone'", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [], + }, + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "EDIT: Change 'hello world' to 'hello anyone'", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + ], "embeds": [ { "name": "image.jpg", @@ -2271,11 +2319,11 @@ mod routes { "author": { "id": CONTRIBUTOR_DID, }, - "assignees": [], "title": "Issue #1", "state": { "status": "open", }, + "assignees": [], "discussion": [ { "id": ISSUE_DISCUSSION_ID, @@ -2283,6 +2331,16 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "Change 'hello world' to 'hello everyone'", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "Change 'hello world' to 'hello everyone'", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [], + }, + ], "embeds": [], "reactions": [], "timestamp": TIMESTAMP, @@ -2295,6 +2353,21 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "This is a reply to the first comment", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "This is a reply to the first comment", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + ], "embeds": [ { "name": "image.jpg", @@ -2931,6 +3004,34 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "EDIT: This is a root level comment", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "This is a root level comment", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "EDIT: This is a root level comment", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + ], "embeds": [ { "name": "image.jpg", @@ -2948,6 +3049,16 @@ mod routes { "id": CONTRIBUTOR_DID, }, "body": "This is a root level comment", + "edits": [ + { + "author": { + "id": CONTRIBUTOR_DID, + }, + "body": "This is a root level comment", + "timestamp": COMMENT_TIMESTAMP, + "embeds": [], + }, + ], "embeds": [], "reactions": [], "timestamp": TIMESTAMP, @@ -3132,6 +3243,30 @@ mod routes { ], "resolved": true, "body": "EDIT: This is a comment on a review", + "edits": [ + { + "author": "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "timestamp": COMMENT_TIMESTAMP, + "body": "This is a comment on a review", + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + { + "author": "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8", + "timestamp": COMMENT_TIMESTAMP, + "body": "EDIT: This is a comment on a review", + "embeds": [ + { + "name": "image.jpg", + "content": "git:94381b429d7f7fe87e1bade52d893ab348ae29cc", + }, + ], + }, + ], "embeds": [ { "name": "image.jpg", diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index d1431c73..22c8a837 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -37,6 +37,7 @@ pub const ISSUE_DISCUSSION_ID: &str = "d11b03464c9bf58b09460713c8e2c74f0725d3bd" pub const ISSUE_COMMENT_ID: &str = "e3aa754cb47d2299781fed49cc91c4b41e06c219"; pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy"; pub const TIMESTAMP: u64 = 1671125284; +pub const COMMENT_TIMESTAMP: u64 = 1671125284000; pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG"; pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index 487144b5..c1d320b0 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -356,7 +356,7 @@ impl Issue { )?; } Action::CommentEdit { id, body, embeds } => { - thread::edit(&mut self.thread, entry, id, timestamp, body, embeds)?; + thread::edit(&mut self.thread, entry, author, id, timestamp, body, embeds)?; } Action::CommentRedact { id } => { let (root, _) = self.root(); diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 66faba78..e6ec5a83 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -881,6 +881,7 @@ impl Patch { thread::edit( &mut review.comments, entry, + author, comment, timestamp, body, @@ -1036,6 +1037,7 @@ impl Patch { thread::edit( &mut revision.discussion, entry, + author, comment, timestamp, body, diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index 4a72d556..337d604c 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -53,8 +53,10 @@ pub type CommentId = EntryId; pub type Reactions = BTreeSet<(ActorId, Reaction)>; /// A comment edit is just some text and an edit time. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct Edit { + /// Edit author. + pub author: ActorId, /// When the edit was made. pub timestamp: Timestamp, /// Edit contents. Replaces previous edits. @@ -86,7 +88,7 @@ impl Serialize for Comment { where S: serde::ser::Serializer, { - let mut state = serializer.serialize_struct("Comment", 8)?; + let mut state = serializer.serialize_struct("Comment", 9)?; state.serialize_field("author", &self.author())?; if let Some(loc) = &self.location { state.serialize_field("location", loc)?; @@ -97,6 +99,7 @@ impl Serialize for Comment { state.serialize_field("reactions", &self.reactions)?; state.serialize_field("resolved", &self.resolved)?; state.serialize_field("body", self.body())?; + state.serialize_field("edits", &self.edits)?; if let Some(location) = self.location() { state.serialize_field("location", &location)?; } @@ -120,6 +123,7 @@ impl Comment { timestamp: Timestamp, ) -> Self { let edit = Edit { + author, body, embeds, timestamp, @@ -168,8 +172,15 @@ impl Comment { } /// Add an edit. - pub fn edit(&mut self, body: String, embeds: Vec>, timestamp: Timestamp) { + pub fn edit( + &mut self, + author: ActorId, + body: String, + embeds: Vec>, + timestamp: Timestamp, + ) { self.edits.push(Edit { + author, body, embeds, timestamp, @@ -336,7 +347,7 @@ impl Thread { comment(self, entry, author, timestamp, body, reply_to, None, vec![])?; } Action::Edit { id, body } => { - edit(self, entry, id, timestamp, body, vec![])?; + edit(self, entry, author, id, timestamp, body, vec![])?; } Action::Redact { id } => { redact(self, entry, id)?; @@ -471,6 +482,7 @@ pub fn comment( pub fn edit( thread: &mut Thread>, id: EntryId, + author: ActorId, comment: EntryId, timestamp: Timestamp, body: String, @@ -489,7 +501,7 @@ pub fn edit( // that as an error. if let Some(comment) = thread.comments.get_mut(&comment) { if let Some(comment) = comment { - comment.edit(body, embeds, timestamp); + comment.edit(author, body, embeds, timestamp); } } else { return Err(Error::Missing(comment)); @@ -785,7 +797,18 @@ mod tests { let (second_id, second) = thread.comments().nth(2).unwrap(); assert!(first_id != second_id); // The ids are not the same, - assert_eq!(first.edits, second.edits); // despite the content being the same. + assert_eq!( + first + .edits + .iter() + .map(|e| (&e.body, e.timestamp)) + .collect::>(), + second + .edits + .iter() + .map(|e| (&e.body, e.timestamp)) + .collect::>(), + ); // despite the content being the same. } #[quickcheck]