v0.3.4: Comment edit/delete, native notifications, forward-compatible protocol, UI fixes
Comment edit & delete: - EditComment/DeleteComment BlobHeaderDiffOps with upstream+downstream propagation - Trust-based: comment author can edit/delete, post author can delete - Storage: edit_comment(), delete_comment() methods - Frontend: inline edit (Enter/Escape), delete with confirm Native notifications: - tauri-plugin-notification for system notifications on all platforms - Triggers for messages, new posts, reactions, and comments - notif_reacts setting added, button-group toggles replace dropdowns - _notifReady flag prevents startup spam Protocol hardening: - BlobHeaderDiffOp::Unknown variant with #[serde(other)] for forward compatibility - Old nodes silently skip unknown ops instead of crashing UI fixes: - Self removed from Following list - Offline follows in lightbox popup (auto-show if no one online) - Sent DMs filtered from My Posts - Comment threading scoped to closest .post (fixes duplicate ID issue) - Select dropdown text legible in WebKitGTK (black on white options) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ce176a2299
commit
0abc244ee9
18 changed files with 1616 additions and 67 deletions
|
|
@ -5493,6 +5493,18 @@ impl ConnectionManager {
|
|||
}
|
||||
let _ = storage.store_comment(comment);
|
||||
}
|
||||
BlobHeaderDiffOp::EditComment { author, post_id, timestamp_ms, new_content } => {
|
||||
// Trust-based: only the comment author can edit
|
||||
if *author == sender || sender == payload.author {
|
||||
let _ = storage.edit_comment(author, post_id, *timestamp_ms, new_content);
|
||||
}
|
||||
}
|
||||
BlobHeaderDiffOp::DeleteComment { author, post_id, timestamp_ms } => {
|
||||
// Trust-based: comment author or post author can delete
|
||||
if *author == sender || sender == payload.author {
|
||||
let _ = storage.delete_comment(author, post_id, *timestamp_ms);
|
||||
}
|
||||
}
|
||||
BlobHeaderDiffOp::SetPolicy(new_policy) => {
|
||||
if sender == payload.author {
|
||||
let _ = storage.set_comment_policy(&payload.post_id, new_policy);
|
||||
|
|
@ -5504,6 +5516,7 @@ impl ConnectionManager {
|
|||
parent_post_id: payload.post_id,
|
||||
});
|
||||
}
|
||||
BlobHeaderDiffOp::Unknown => {} // future ops — silently skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3229,6 +3229,88 @@ impl Node {
|
|||
Ok(comment)
|
||||
}
|
||||
|
||||
/// Edit one of your own comments on a post.
|
||||
pub async fn edit_comment(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
timestamp_ms: u64,
|
||||
new_content: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let our_node_id = self.node_id;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64;
|
||||
|
||||
let storage = self.storage.lock().await;
|
||||
storage.edit_comment(&our_node_id, &post_id, timestamp_ms, &new_content)?;
|
||||
drop(storage);
|
||||
|
||||
// Propagate via BlobHeaderDiff
|
||||
{
|
||||
let network = &self.network;
|
||||
let diff = crate::protocol::BlobHeaderDiffPayload {
|
||||
post_id,
|
||||
author: our_node_id,
|
||||
ops: vec![crate::types::BlobHeaderDiffOp::EditComment {
|
||||
author: our_node_id,
|
||||
post_id,
|
||||
timestamp_ms,
|
||||
new_content,
|
||||
}],
|
||||
timestamp_ms: now,
|
||||
};
|
||||
network.propagate_engagement_diff(&post_id, &diff, &our_node_id).await;
|
||||
let upstream = {
|
||||
let storage = self.storage.lock().await;
|
||||
storage.get_post_upstream(&post_id).ok().flatten()
|
||||
};
|
||||
if let Some(up) = upstream {
|
||||
let _ = network.send_to_peer_uni(&up, crate::protocol::MessageType::BlobHeaderDiff, &diff).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete one of your own comments on a post.
|
||||
pub async fn delete_comment(
|
||||
&self,
|
||||
post_id: PostId,
|
||||
timestamp_ms: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
let our_node_id = self.node_id;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_millis() as u64;
|
||||
|
||||
let storage = self.storage.lock().await;
|
||||
storage.delete_comment(&our_node_id, &post_id, timestamp_ms)?;
|
||||
drop(storage);
|
||||
|
||||
// Propagate via BlobHeaderDiff
|
||||
{
|
||||
let network = &self.network;
|
||||
let diff = crate::protocol::BlobHeaderDiffPayload {
|
||||
post_id,
|
||||
author: our_node_id,
|
||||
ops: vec![crate::types::BlobHeaderDiffOp::DeleteComment {
|
||||
author: our_node_id,
|
||||
post_id,
|
||||
timestamp_ms,
|
||||
}],
|
||||
timestamp_ms: now,
|
||||
};
|
||||
network.propagate_engagement_diff(&post_id, &diff, &our_node_id).await;
|
||||
let upstream = {
|
||||
let storage = self.storage.lock().await;
|
||||
storage.get_post_upstream(&post_id).ok().flatten()
|
||||
};
|
||||
if let Some(up) = upstream {
|
||||
let _ = network.send_to_peer_uni(&up, crate::protocol::MessageType::BlobHeaderDiff, &diff).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all comments for a post.
|
||||
pub async fn get_comments(&self, post_id: PostId) -> anyhow::Result<Vec<crate::types::InlineComment>> {
|
||||
let storage = self.storage.lock().await;
|
||||
|
|
|
|||
|
|
@ -3918,6 +3918,24 @@ impl Storage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Edit a comment (must match author + post_id + timestamp_ms).
|
||||
pub fn edit_comment(&self, author: &NodeId, post_id: &PostId, timestamp_ms: u64, new_content: &str) -> anyhow::Result<bool> {
|
||||
let updated = self.conn.execute(
|
||||
"UPDATE comments SET content = ?4 WHERE author = ?1 AND post_id = ?2 AND timestamp_ms = ?3",
|
||||
params![author.as_slice(), post_id.as_slice(), timestamp_ms as i64, new_content],
|
||||
)?;
|
||||
Ok(updated > 0)
|
||||
}
|
||||
|
||||
/// Delete a comment (must match author + post_id + timestamp_ms).
|
||||
pub fn delete_comment(&self, author: &NodeId, post_id: &PostId, timestamp_ms: u64) -> anyhow::Result<bool> {
|
||||
let deleted = self.conn.execute(
|
||||
"DELETE FROM comments WHERE author = ?1 AND post_id = ?2 AND timestamp_ms = ?3",
|
||||
params![author.as_slice(), post_id.as_slice(), timestamp_ms as i64],
|
||||
)?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
/// Get all comments for a post, ordered by timestamp.
|
||||
pub fn get_comments(&self, post_id: &PostId) -> anyhow::Result<Vec<InlineComment>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
|
|
|
|||
|
|
@ -817,8 +817,13 @@ pub enum BlobHeaderDiffOp {
|
|||
AddReaction(Reaction),
|
||||
RemoveReaction { reactor: NodeId, emoji: String, post_id: PostId },
|
||||
AddComment(InlineComment),
|
||||
EditComment { author: NodeId, post_id: PostId, timestamp_ms: u64, new_content: String },
|
||||
DeleteComment { author: NodeId, post_id: PostId, timestamp_ms: u64 },
|
||||
SetPolicy(CommentPolicy),
|
||||
ThreadSplit { new_post_id: PostId },
|
||||
/// Unknown ops from newer protocol versions — silently ignored
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Aggregated engagement header for a post (stored locally, propagated as diffs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue