Fix pasted images not rendering

Pasting a single image went through save_embed_by_clipboard, which
stored the clipboard's raw RGBA pixels instead of an encoded image.
The result could not be decoded, so the embed showed as a broken image.
Always store the pasted file's own bytes instead. Nothing else uses
save_embed_by_clipboard, so it's removed.

Co-Authored-By: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
Brandon Oxendine
2026-10-06 15:04:57 +02:00
committed by Rūdolfs Ošiņš
co-authored by Rūdolfs Ošiņš
parent 118abffdea
commit 8175823ab8
8 changed files with 45 additions and 40 deletions
Generated
-1
View File
@@ -4374,7 +4374,6 @@ dependencies = [
"sqlite",
"ssh-key",
"tauri",
"tauri-plugin-clipboard-manager",
"tauri-plugin-fs",
"tempfile",
"thiserror 2.0.19",
-16
View File
@@ -5,7 +5,6 @@ use radicle::identity;
use radicle_types as types;
use radicle_types::error::Error;
use radicle_types::traits::thread::Thread;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_dialog::DialogExt;
use crate::AppState;
@@ -33,21 +32,6 @@ pub async fn save_embed_by_path(
ctx.save_embed_by_path(rid, path)
}
#[tauri::command]
pub async fn save_embed_by_clipboard(
app_handle: tauri::AppHandle,
ctx: tauri::State<'_, AppState>,
rid: identity::RepoId,
name: String,
) -> Result<git::Oid, Error> {
let content = app_handle
.clipboard()
.read_image()
.map(|i| i.rgba().to_vec())?;
ctx.save_embed_by_bytes(rid, name, content)
}
#[tauri::command]
pub async fn save_embed_by_bytes(
ctx: tauri::State<'_, AppState>,
-1
View File
@@ -88,7 +88,6 @@ pub fn run() {
cob::patch::rebuild_patch_cache,
cob::patch::revisions_by_patch,
cob::save_embed_by_bytes,
cob::save_embed_by_clipboard,
cob::save_embed_by_path,
cob::save_embed_to_disk,
diff::get_diff,
-1
View File
@@ -23,7 +23,6 @@ serde_json = { version = "1.0.0" }
sqlite = { version = "0.37.0", features = ["bundled"] }
ssh-key = { version = "0.6.3" }
tauri = { version = "2.11.1", features = ["isolation"] }
tauri-plugin-clipboard-manager = { version = "2.3.2" }
tauri-plugin-fs = { version = "2.5.0" }
tempfile = { version = "3.19.0" }
thiserror = { version = "2.0.12" }
-4
View File
@@ -54,10 +54,6 @@ pub enum Error {
#[error(transparent)]
AliasError(#[from] radicle::node::AliasError),
/// Tauri Plugin Clipboard error.
#[error(transparent)]
TauriPluginClipboard(#[from] tauri_plugin_clipboard_manager::Error),
/// Tauri Plugin Fs error.
#[error(transparent)]
TauriPluginFs(#[from] tauri_plugin_fs::Error),
+1 -4
View File
@@ -6,10 +6,7 @@ use std::fs;
use std::path::{Path, PathBuf};
/// Tauri commands with no HTTP route, and why.
const TAURI_ONLY: &[(&str, &str)] = &[(
"save_embed_by_clipboard",
"reads the native clipboard, which the HTTP server has no access to",
)];
const TAURI_ONLY: &[(&str, &str)] = &[];
/// HTTP routes with no Tauri command, and why.
const HTTP_ONLY: &[(&str, &str)] = &[(
+5 -13
View File
@@ -242,19 +242,11 @@
if (e.clipboardData?.files && e.clipboardData.files.length > 0) {
e.preventDefault();
const files = Array.from(e.clipboardData.files);
// A single item may be an image the webview can't hand over, so the
// backend reads it from the clipboard itself.
if (e.clipboardData.items.length === 1) {
await uploadEmbeds(
files.slice(0, 1),
file => file.name,
file =>
invoke<string>("save_embed_by_clipboard", { name: file.name, rid }),
);
} else {
await uploadEmbeds(files, file => file.name, saveFileBytes);
}
await uploadEmbeds(
Array.from(e.clipboardData.files),
file => file.name,
saveFileBytes,
);
}
}
+39
View File
@@ -1,13 +1,18 @@
import type { EmbedWithMimeType } from "@bindings/cob/EmbedWithMimeType";
import type { JSHandle, Locator, Page } from "@playwright/test";
import type { RadiclePeer } from "@tests/support/peerManager.js";
import { createProject } from "@tests/support/collaboration.js";
import { expect, test, waitForCommand } from "@tests/support/fixtures.js";
const onePixelPng =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
async function openIssue(page: Page, peer: RadiclePeer) {
const { rid, issueId } = await createProject(peer);
await page.goto(`/repos/${rid}/issues/${issueId}`);
await expect(page.getByText("Nobody reads them.")).toBeVisible();
return rid;
}
function fileTransfer(
@@ -86,3 +91,37 @@ test("the drop highlight clears once the file leaves", async ({
await highlight.dispatchEvent("dragleave", { dataTransfer: transfer });
await expect(highlight).toBeHidden();
});
test("a pasted image is stored as the image itself", async ({ page, peer }) => {
const rid = await openIssue(page, peer);
const composer = page.getByPlaceholder("Leave a comment");
await waitForCommand(page, "save_embed_by_bytes", () =>
composer.evaluate((textarea, png) => {
const clipboardData = new DataTransfer();
const bytes = Uint8Array.from(atob(png), c => c.charCodeAt(0));
clipboardData.items.add(
new File([bytes], "image.png", { type: "image/png" }),
);
textarea.dispatchEvent(
new ClipboardEvent("paste", {
clipboardData,
bubbles: true,
cancelable: true,
}),
);
}, onePixelPng),
);
await expect(composer).toHaveValue(/\[image\.png\]\(\w+\)/);
const oid = /\[image\.png\]\((\w+)\)/.exec(await composer.inputValue())?.[1];
const response = await page.request.post(
`http://127.0.0.1:${peer.httpdBaseUrl.port}/get_embed`,
{ data: { rid, oid, name: "image.png" } },
);
const embed = (await response.json()) as EmbedWithMimeType;
expect(embed.mimeType).toBe("image/png");
expect(Buffer.from(embed.content)).toEqual(
Buffer.from(onePixelPng, "base64"),
);
});