From f27e5a0d891dda0185a64a606f337222cd6f8ab4 Mon Sep 17 00:00:00 2001 From: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Date: Tue, 25 Aug 2026 13:33:22 -0400 Subject: [PATCH] fix(canvas): address canvas history review nits Gate canvas existence on the persisted revision id, not content truthiness: an empty-string canvas is a valid kind:40100 revision (and restore can republish one), so keying existence, the Create/Edit label, and the History section on content hid retained history for an existing empty canvas. Bound get_canvas_history's page size to the relay read maximum. A request above 1,000 is silently clamped by the relay, which made `events.len() == page_size` false and nulled the cursor even when older revisions remained, stranding them behind an unreachable page. Reject outside 1..=1000. Reset the shared restore mutation on revision selection change so a failed restore's error can no longer render under a different row. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src-tauri/src/commands/canvas.rs | 43 ++++- .../channels/ui/CanvasHistoryPanel.tsx | 10 +- .../features/channels/ui/ChannelCanvas.tsx | 13 +- .../ui/ChannelCanvasEmptyExistence.test.mjs | 158 ++++++++++++++++++ 4 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 desktop/src/features/channels/ui/ChannelCanvasEmptyExistence.test.mjs diff --git a/desktop/src-tauri/src/commands/canvas.rs b/desktop/src-tauri/src/commands/canvas.rs index 08c8bc15a3..cd113f10f1 100644 --- a/desktop/src-tauri/src/commands/canvas.rs +++ b/desktop/src-tauri/src/commands/canvas.rs @@ -119,7 +119,11 @@ pub async fn get_canvas_history( if before_id.is_some() && until.is_none() { return Err("before_id requires until".to_string()); } - let page_size = limit.unwrap_or(100).max(1); + // Bound the page size to the relay's read maximum. Beyond 1,000 the relay + // silently clamps the returned rows, which would make `events.len() == + // page_size` false and null the cursor even when older revisions remain, + // stranding them behind an unreachable page. + let page_size = resolve_history_page_size(limit)?; let mut filter = serde_json::json!({ "kinds": [40100], @@ -169,3 +173,40 @@ pub async fn get_canvas_history( "next_cursor": next_cursor, })) } + +/// Resolve and validate the history page size against the relay's read +/// maximum. Defaults to 100 when unset; a value outside `1..=1000` is rejected +/// so cursor generation is never based on a size the relay would silently +/// clamp (which strands older revisions behind a falsely-terminated page). +fn resolve_history_page_size(limit: Option) -> Result { + let page_size = limit.unwrap_or(100); + if !(1..=1000).contains(&page_size) { + return Err("limit must be between 1 and 1000".to_string()); + } + Ok(page_size) +} + +#[cfg(test)] +mod tests { + use super::resolve_history_page_size; + + #[test] + fn defaults_to_100_when_unset() { + assert_eq!(resolve_history_page_size(None).unwrap(), 100); + } + + #[test] + fn rejects_zero() { + assert!(resolve_history_page_size(Some(0)).is_err()); + } + + #[test] + fn accepts_relay_maximum() { + assert_eq!(resolve_history_page_size(Some(1000)).unwrap(), 1000); + } + + #[test] + fn rejects_above_relay_maximum() { + assert!(resolve_history_page_size(Some(1001)).is_err()); + } +} diff --git a/desktop/src/features/channels/ui/CanvasHistoryPanel.tsx b/desktop/src/features/channels/ui/CanvasHistoryPanel.tsx index 2d50ac8219..a6220fb5a8 100644 --- a/desktop/src/features/channels/ui/CanvasHistoryPanel.tsx +++ b/desktop/src/features/channels/ui/CanvasHistoryPanel.tsx @@ -110,9 +110,13 @@ export function CanvasHistoryPanel({ ) : null} - {canvasContent ? ( + {canvasExists ? ( <>