diff --git a/Cargo.lock b/Cargo.lock index e6ae2b11e..838c4c736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4374,7 +4374,6 @@ dependencies = [ "sqlite", "ssh-key", "tauri", - "tauri-plugin-clipboard-manager", "tauri-plugin-fs", "tempfile", "thiserror 2.0.19", diff --git a/crates/radicle-tauri/src/commands/cob.rs b/crates/radicle-tauri/src/commands/cob.rs index aba884690..149cd9f76 100644 --- a/crates/radicle-tauri/src/commands/cob.rs +++ b/crates/radicle-tauri/src/commands/cob.rs @@ -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 { - 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>, diff --git a/crates/radicle-tauri/src/lib.rs b/crates/radicle-tauri/src/lib.rs index 747a09348..9fcaf7e9f 100644 --- a/crates/radicle-tauri/src/lib.rs +++ b/crates/radicle-tauri/src/lib.rs @@ -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, diff --git a/crates/radicle-types/Cargo.toml b/crates/radicle-types/Cargo.toml index 41c0a227d..924dc138a 100644 --- a/crates/radicle-types/Cargo.toml +++ b/crates/radicle-types/Cargo.toml @@ -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" } diff --git a/crates/radicle-types/src/error.rs b/crates/radicle-types/src/error.rs index a1fb11903..85c1fa240 100644 --- a/crates/radicle-types/src/error.rs +++ b/crates/radicle-types/src/error.rs @@ -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), diff --git a/crates/test-http-api/tests/parity.rs b/crates/test-http-api/tests/parity.rs index 24a327623..2cdb6a397 100644 --- a/crates/test-http-api/tests/parity.rs +++ b/crates/test-http-api/tests/parity.rs @@ -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)] = &[( diff --git a/src/components/ExtendedTextarea.svelte b/src/components/ExtendedTextarea.svelte index 5aa36530b..39ddd716f 100644 --- a/src/components/ExtendedTextarea.svelte +++ b/src/components/ExtendedTextarea.svelte @@ -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("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, + ); } } diff --git a/tests/e2e/repo/fileDrop.spec.ts b/tests/e2e/repo/fileDrop.spec.ts index fb438796c..80abdde10 100644 --- a/tests/e2e/repo/fileDrop.spec.ts +++ b/tests/e2e/repo/fileDrop.spec.ts @@ -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"), + ); +});