From f389fcf46838cf6206a82d748859b1ec86b4cd03 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Thu, 14 May 2026 12:01:30 +0200 Subject: [PATCH 1/2] :bug: Fix problem with copy-as-image action (#9586) --- exporter/src/app/handlers/export_shapes.cljs | 4 +- .../app/main/data/workspace/clipboard.cljs | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/exporter/src/app/handlers/export_shapes.cljs b/exporter/src/app/handlers/export_shapes.cljs index 901a2f2bb2..1789ba9531 100644 --- a/exporter/src/app/handlers/export_shapes.cljs +++ b/exporter/src/app/handlers/export_shapes.cljs @@ -64,7 +64,7 @@ (defn- handle-single-export [{:keys [:request/auth-token] :as exchange} {:keys [export name skip-children is-wasm] :as params}] (let [resource (rsc/create (:type export) (or name (:name export))) - export (assoc export :skip-children skip-children :is-wasm is-wasm)] + export (assoc export :skip-children skip-children :is-wasm (boolean is-wasm))] (->> (rd/render export (fn [{:keys [path] :as object}] @@ -112,7 +112,7 @@ (rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_"))) proc (->> exports - (map (fn [export] (rd/render (assoc export :is-wasm is-wasm) append))) + (map (fn [export] (rd/render (assoc export :is-wasm (boolean is-wasm)) append))) (p/all) (p/mcat (fn [_] (rsc/close-zip zip))) (p/fmap (constantly resource)) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index bc8d88a746..f67b04b98e 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -34,6 +34,7 @@ [app.config :as cf] [app.main.data.changes :as dch] [app.main.data.event :as ev] + [app.main.data.exports.wasm :as wasm.exports] [app.main.data.helpers :as dsh] [app.main.data.notifications :as ntf] [app.main.data.persistence :as-alias dps] @@ -1131,10 +1132,11 @@ :enabled true :name ""} - params {:exports [export] - :profile-id (:profile-id state) - :cmd :export-shapes - :wait true}] + ;; Create a deferred promise immediately, before any async operations. + ;; Registering the clipboard write NOW preserves the user-gesture security + ;; context; the actual blob is supplied asynchronously once the export finishes. + deferred (p/deferred) + write-promise (clipboard/to-clipboard-promise "image/png" deferred)] (rx/concat ;; Ensure current state persisted before exporting. @@ -1144,21 +1146,34 @@ (rx/first) (rx/timeout 400 (rx/empty))) - ;; Exporting itself can time its time, better to notify that we are busy. + ;; Exporting itself can take its time, better to notify that we are busy. (rx/of (ntf/info (tr "workspace.clipboard.copying"))) - ;; Call exporter to get image URI, then fetch and copy blob. - (->> (rp/cmd! :export params) + ;; Call exporter to get image URI, then fetch blob and resolve the deferred. + (->> (if (and (features/active-feature? state "render-wasm/v1") + (contains? cf/flags :wasm-export)) + (rx/of {:uri (wasm.exports/export-image-uri export)}) + (rp/cmd! :export + {:exports [export] + :profile-id (:profile-id state) + :cmd :export-shapes + :wait true})) + (rx/mapcat (fn [{:keys [uri]}] (http/send! {:method :get :uri uri :response-type :blob}))) (rx/map :body) - (rx/tap (fn [blob] - (clipboard/to-clipboard-promise "image/png" (p/resolved blob)))) + (rx/mapcat (fn [blob] + ;; Resolve the deferred with the fetched blob; the browser + ;; will now complete the clipboard write it started earlier. + (p/resolve! deferred blob) + (rx/from write-promise))) (rx/map (fn [_] (ntf/success (tr "workspace.clipboard.image-copied")))) (rx/catch (fn [e] - (js/console.error "clipboard blocked:" e) - (ntf/error (tr "workspace.clipboard.image-copy-failed")) - (rx/empty))))))))) + (js/console.error "clipboard error:" e) + ;; Reject the deferred in case the error occurred before the + ;; blob was fetched, so the pending clipboard write is cancelled. + (p/reject! deferred e) + (rx/of (ntf/error (tr "workspace.clipboard.image-copy-failed"))))))))))) From 78e3077a37edd0fff8827e4c13e32e0fe9bccda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 14 May 2026 12:42:58 +0200 Subject: [PATCH 2/2] :wrench: Use polyfilled helpers instead of raf (#9628) --- frontend/src/app/main/data/event.cljs | 3 ++- .../src/app/main/data/workspace/thumbnails_wasm.cljs | 5 +++-- frontend/src/app/main/ui/ds/tooltip/tooltip.cljs | 2 +- .../management/forms/controls/floating_dropdown.cljs | 3 ++- .../app/main/ui/workspace/viewport/pixel_overlay.cljs | 5 +++-- frontend/src/app/render_wasm/api.cljs | 7 ++++--- frontend/src/app/util/timers.cljs | 11 ++++++++++- 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/data/event.cljs b/frontend/src/app/main/data/event.cljs index b8c439f553..aa9df88085 100644 --- a/frontend/src/app/main/data/event.cljs +++ b/frontend/src/app/main/data/event.cljs @@ -23,6 +23,7 @@ [app.util.object :as obj] [app.util.perf :as perf] [app.util.storage :as storage] + [app.util.timers :as timers] [beicon.v2.core :as rx] [beicon.v2.operators :as rxo] [cuerdas.core :as str] @@ -216,7 +217,7 @@ (rx/create (fn [subs] (let [start (perf/now)] - (js/requestAnimationFrame + (timers/raf #(.postTask js/scheduler (fn [] (let [time (- (perf/now) start)] diff --git a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs index 507714578c..612bdd2e53 100644 --- a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs @@ -25,6 +25,7 @@ [app.main.repo :as rp] [app.main.store :as st] [app.render-wasm.api :as wasm.api] + [app.util.timers :as timers] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -58,7 +59,7 @@ (rx/create (fn [subs] (let [req-id - (js/requestAnimationFrame + (timers/raf (fn [_] (try (let [objects (dsh/lookup-page-objects @st/state file-id page-id)] @@ -83,7 +84,7 @@ (rx/error! subs "Frame not found"))) (catch :default err (rx/error! subs err)))))] - #(js/cancelAnimationFrame req-id))))) + #(timers/cancel-af! req-id))))) (defn render-thumbnail "Renders a component thumbnail via WASM and updates the UI immediately. diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 10e9638cf2..3eb9a4f939 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -322,7 +322,7 @@ (let [trigger-el (mf/ref-val trigger-ref) tooltip-el (mf/ref-val tooltip-ref)] (when (and trigger-el tooltip-el) - (js/requestAnimationFrame + (ts/raf (fn [] (let [origin-brect (dom/get-bounding-rect trigger-el) tooltip-brect (dom/get-bounding-rect tooltip-el) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs index d7cf90a3f3..cddb3b9eb4 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/floating_dropdown.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.management.forms.controls.floating-dropdown (:require [app.util.dom :as dom] + [app.util.timers :as timers] [rumext.v2 :as mf])) (defn use-floating-dropdown [is-open input-wrapper-ref outer-wrapper-ref dropdown-ref] @@ -48,7 +49,7 @@ (when is-open (let [recalculate (fn [] - (js/requestAnimationFrame + (timers/raf (fn [] (let [input-node (mf/ref-val input-wrapper-ref)] (calculate-position input-node))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index 1cf70067a6..89f1f9737c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -22,6 +22,7 @@ [app.util.globals :as ug] [app.util.keyboard :as kbd] [app.util.object :as obj] + [app.util.timers :as timers] [beicon.v2.core :as rx] [goog.events :as events] [rumext.v2 :as mf])) @@ -96,7 +97,7 @@ ;; Store latest color synchronously so the click handler always reads ;; the correct pixel even before the rAF fires (fixes race condition) (mf/set-ref-val! last-picked-color color) - (js/requestAnimationFrame + (timers/raf (fn [] (st/emit! (dwc/pick-color color)))))))))) @@ -304,7 +305,7 @@ ;; the correct pixel even before the rAF fires (fixes race condition) (mf/set-ref-val! last-picked-color color) ;; rAF throttles state updates to avoid an infinite React re-render loop - (js/requestAnimationFrame + (timers/raf (fn [] (st/emit! (dwc/pick-color color)))))))))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index f3b6cdc244..6924456411 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -53,6 +53,7 @@ [app.util.i18n :refer [tr]] [app.util.modules :as mod] [app.util.text.content :as tc] + [app.util.timers :as timers] [beicon.v2.core :as rx] [cuerdas.core :as str] [promesa.core :as p] @@ -398,7 +399,7 @@ (when-not @pending-render (reset! pending-render true) (let [frame-id - (js/requestAnimationFrame + (timers/raf (fn [ts] (reset! pending-render false) (set! wasm/internal-frame-id nil) @@ -1708,7 +1709,7 @@ [] (p/create (fn [resolve _reject] - (js/requestAnimationFrame (fn [] (resolve nil)))))) + (timers/raf (fn [] (resolve nil)))))) (def ^:private default-context-options #js {:antialias false @@ -1878,7 +1879,7 @@ ;; Cancel any pending animation frame to prevent race conditions. (when wasm/internal-frame-id - (js/cancelAnimationFrame wasm/internal-frame-id)) + (timers/cancel-af! wasm/internal-frame-id)) ;; Reset render flags to prevent new renders from being scheduled. (reset! pending-render false) diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index f943d35457..afc7e79daf 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -65,11 +65,20 @@ #(.requestAnimationFrame js/globalThis %) #(js/setTimeout % 16))) +(def ^:private cancel-animation-frame + (if (and (exists? js/globalThis) + (exists? (.-cancelAnimationFrame js/globalThis))) + #(.cancelAnimationFrame js/globalThis %) + #(js/clearTimeout %))) + (defn raf [f] (^function request-animation-frame f)) +(defn cancel-af! + [frame-id] + (^function cancel-animation-frame frame-id)) + (defn idle-then-raf [f] (schedule-on-idle #(^function raf f))) -