diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj
index c8315b37c..8a1f5cb72 100644
--- a/backend/src/app/rpc/commands/files_thumbnails.clj
+++ b/backend/src/app/rpc/commands/files_thumbnails.clj
@@ -48,8 +48,9 @@
(let [sql (str/concat
"select object_id, data, media_id "
" from file_object_thumbnail"
- " where file_id=?")]
- (->> (db/exec! conn [sql file-id])
+ " where file_id=?")
+ res (db/exec! conn [sql file-id])]
+ (->> res
(d/index-by :object-id (fn [row]
(or (some-> row :media-id get-public-uri)
(:data row))))
@@ -57,14 +58,16 @@
([conn file-id object-ids]
(let [sql (str/concat
- "select object_id, data "
+ "select object_id, data, media_id "
" from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)")
- ids (db/create-array conn "text" (seq object-ids))]
- (->> (db/exec! conn [sql file-id ids])
- (d/index-by :object-id (fn [row]
- (or (some-> row :media-id get-public-uri)
- (:data row))))))))
+ ids (db/create-array conn "text" (seq object-ids))
+ res (db/exec! conn [sql file-id ids])]
+ (d/index-by :object-id
+ (fn [row]
+ (or (some-> row :media-id get-public-uri)
+ (:data row)))
+ res))))
(sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails."
@@ -248,125 +251,200 @@
;; --- MUTATION COMMAND: upsert-file-object-thumbnail
-(def sql:upsert-object-thumbnail-1
+(def sql:upsert-object-thumbnail
"insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?)
on conflict(file_id, object_id) do
update set data = ?;")
-(def sql:upsert-object-thumbnail-2
- "insert into file_object_thumbnail(file_id, object_id, media_id)
- values (?, ?, ?)
- on conflict(file_id, object_id) do
- update set media_id = ?;")
-
(defn upsert-file-object-thumbnail!
- [{:keys [::db/conn ::sto/storage]} {:keys [file-id object-id] :as params}]
+ [conn {:keys [file-id object-id data]}]
+ (if data
+ (db/exec-one! conn [sql:upsert-object-thumbnail file-id object-id data data])
+ (db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))
- ;; NOTE: params can come with data set but with `nil` value, so we
- ;; need first check the existence of the key and then the value.
- (cond
- (contains? params :data)
- (if-let [data (:data params)]
- (db/exec-one! conn [sql:upsert-object-thumbnail-1 file-id object-id data data])
- (db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))
-
- (contains? params :media)
- (if-let [{:keys [path mtype] :as media} (:media params)]
- (let [_ (media/validate-media-type! media)
- _ (media/validate-media-size! media)
- hash (sto/calculate-hash path)
- data (-> (sto/content path)
- (sto/wrap-with-hash hash))
- media (sto/put-object! storage
- {::sto/content data
- ::sto/deduplicate? false
- :content-type mtype
- :bucket "file-object-thumbnail"})]
-
- (db/exec-one! conn [sql:upsert-object-thumbnail-2 file-id object-id (:id media) (:id media)]))
- (db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id}))))
-
-;; FIXME: change it on validation refactor
(s/def ::data (s/nilable ::us/string))
-(s/def ::media (s/nilable ::media/upload))
(s/def ::object-id ::us/string)
(s/def ::upsert-file-object-thumbnail
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id]
- :opt-un [::data ::media]))
+ :opt-un [::data]))
(sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"
+ ::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
+ (db/with-atomic [conn pool]
+ (files/check-edition-permissions! conn profile-id file-id)
- (assert (or (contains? params :data)
- (contains? params :media)))
+ (when-not (db/read-only? conn)
+ (upsert-file-object-thumbnail! conn params)
+ nil)))
+
+
+;; --- MUTATION COMMAND: create-file-object-thumbnail
+
+(def ^:private sql:create-object-thumbnail
+ "insert into file_object_thumbnail(file_id, object_id, media_id)
+ values (?, ?, ?)
+ on conflict(file_id, object_id) do
+ update set media_id = ?;")
+
+(defn- create-file-object-thumbnail!
+ [{:keys [::db/conn ::sto/storage]} file-id object-id media]
+
+ (let [path (:path media)
+ mtype (:mtype media)
+ hash (sto/calculate-hash path)
+ data (-> (sto/content path)
+ (sto/wrap-with-hash hash))
+ media (sto/put-object! storage
+ {::sto/content data
+ ::sto/deduplicate? false
+ :content-type mtype
+ :bucket "file-object-thumbnail"})]
+
+ (db/exec-one! conn [sql:create-object-thumbnail file-id object-id
+ (:id media) (:id media)])))
+
+
+(s/def ::media (s/nilable ::media/upload))
+(s/def ::create-file-object-thumbnail
+ (s/keys :req [::rpc/profile-id]
+ :req-un [::file-id ::object-id ::media]))
+
+(sv/defmethod ::create-file-object-thumbnail
+ {:doc/added "1.19"
+ ::audit/skip true}
+ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
+ (db/with-atomic [conn pool]
+ (files/check-edition-permissions! conn profile-id file-id)
+ (media/validate-media-type! media)
+ (media/validate-media-size! media)
+
+ (when-not (db/read-only? conn)
+ (-> cfg
+ (update ::sto/storage media/configure-assets-storage)
+ (assoc ::db/conn conn)
+ (create-file-object-thumbnail! file-id object-id media))
+ nil)))
+
+;; --- MUTATION COMMAND: delete-file-object-thumbnail
+
+(defn- delete-file-object-thumbnail!
+ [{:keys [::db/conn ::sto/storage]} file-id object-id]
+ (when-let [{:keys [media-id]} (db/get* conn :file-object-thumbnail
+ {:file-id file-id
+ :object-id object-id}
+ {::db/for-update? true})]
+ (when media-id
+ (sto/del-object! storage media-id))
+
+ (db/delete! conn :file-object-thumbnail
+ {:file-id file-id
+ :object-id object-id})
+ nil))
+
+(s/def ::delete-file-object-thumbnail
+ (s/keys :req [::rpc/profile-id]
+ :req-un [::file-id ::object-id]))
+
+(sv/defmethod ::delete-file-object-thumbnail
+ {:doc/added "1.19"
+ ::audit/skip true}
+ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id]}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
- (let [cfg (-> cfg
- (update ::sto/storage media/configure-assets-storage)
- (assoc ::db/conn conn))]
- (upsert-file-object-thumbnail! cfg params)
- nil))))
+ (-> cfg
+ (update ::sto/storage media/configure-assets-storage)
+ (assoc ::db/conn conn)
+ (delete-file-object-thumbnail! file-id object-id))
+ nil)))
;; --- MUTATION COMMAND: upsert-file-thumbnail
(def ^:private sql:upsert-file-thumbnail
- "insert into file_thumbnail (file_id, revn, data, media_id, props)
- values (?, ?, ?, ?, ?::jsonb)
+ "insert into file_thumbnail (file_id, revn, data, props)
+ values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do
- update set data=?, media_id=?, props=?, updated_at=now();")
+ update set data = ?, props=?, updated_at=now();")
(defn- upsert-file-thumbnail!
- [{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props] :as params}]
+ [conn {:keys [file-id revn data props]}]
(let [props (db/tjson (or props {}))]
- (cond
- (contains? params :data)
- (when-let [data (:data params)]
- (db/exec-one! conn [sql:upsert-file-thumbnail
- file-id revn data nil props data nil props]))
+ (db/exec-one! conn [sql:upsert-file-thumbnail
+ file-id revn data props data props])))
- (contains? params :media)
- (when-let [{:keys [path mtype] :as media} (:media params)]
- (let [_ (media/validate-media-type! media)
- _ (media/validate-media-size! media)
- hash (sto/calculate-hash path)
- data (-> (sto/content path)
- (sto/wrap-with-hash hash))
- media (sto/put-object! storage
- {::sto/content data
- ::sto/deduplicate? false
- :content-type mtype
- :bucket "file-thumbnail"})]
- (db/exec-one! conn [sql:upsert-file-thumbnail
- file-id revn nil (:id media) props nil (:id media) props]))))))
(s/def ::revn ::us/integer)
(s/def ::props map?)
-(s/def ::media ::media/upload)
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id]
- :req-un [::file-id ::revn ::props]
- :opt-un [::data ::media]))
+ :req-un [::file-id ::revn ::props ::data]))
(sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails."
{::doc/added "1.17"
+ ::doc/deprecated "1.19"
::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
- (let [cfg (-> cfg
- (update ::sto/storage media/configure-assets-storage)
- (assoc ::db/conn conn))]
- (upsert-file-thumbnail! cfg params))
+ (upsert-file-thumbnail! conn params)
+ nil)))
+
+;; --- MUTATION COMMAND: create-file-thumbnail
+
+(def ^:private sql:create-file-thumbnail
+ "insert into file_thumbnail (file_id, revn, media_id, props)
+ values (?, ?, ?, ?::jsonb)
+ on conflict(file_id, revn) do
+ update set media_id=?, props=?, updated_at=now();")
+
+(defn- create-file-thumbnail!
+ [{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
+ (media/validate-media-type! media)
+ (media/validate-media-size! media)
+
+ (let [props (db/tjson (or props {}))
+ path (:path media)
+ mtype (:mtype media)
+ hash (sto/calculate-hash path)
+ data (-> (sto/content path)
+ (sto/wrap-with-hash hash))
+ media (sto/put-object! storage
+ {::sto/content data
+ ::sto/deduplicate? false
+ :content-type mtype
+ :bucket "file-thumbnail"})]
+ (db/exec-one! conn [sql:create-file-thumbnail file-id revn
+ (:id media) props
+ (:id media) props])))
+
+(s/def ::media ::media/upload)
+(s/def ::create-file-thumbnail
+ (s/keys :req [::rpc/profile-id]
+ :req-un [::file-id ::revn ::props ::media]))
+
+(sv/defmethod ::create-file-thumbnail
+ "Creates or updates the file thumbnail. Mainly used for paint the
+ grid thumbnails."
+ {::doc/added "1.19"
+ ::audit/skip true}
+ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
+ (db/with-atomic [conn pool]
+ (files/check-edition-permissions! conn profile-id file-id)
+ (when-not (db/read-only? conn)
+ (-> cfg
+ (update ::sto/storage media/configure-assets-storage)
+ (assoc ::db/conn conn)
+ (create-file-thumbnail! params))
nil)))
diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj
index 468d8f08e..84687d04f 100644
--- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj
+++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj
@@ -52,7 +52,7 @@
:parent-id uuid/zero
:type :frame}}])
- data1 {::th/type :upsert-file-object-thumbnail
+ data1 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-1"
@@ -61,7 +61,7 @@
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"}}
- data2 {::th/type :upsert-file-object-thumbnail
+ data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id (str page-id shid)
@@ -156,7 +156,7 @@
:revn 1
:data "data:base64,1234123124"}
- data2 {::th/type :upsert-file-thumbnail
+ data2 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
@@ -166,7 +166,7 @@
:path (th/tempfile "backend_tests/test_files/sample2.jpg")
:mtype "image/jpeg"}}
- data3 {::th/type :upsert-file-thumbnail
+ data3 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:props {}
@@ -177,11 +177,11 @@
:mtype "image/jpeg"}}]
(let [out (th/command! data1)]
- ;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [out (th/command! data2)]
+ ;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out))))
@@ -251,10 +251,10 @@
(let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(t/is (nil? row)))
- (t/is (some? (sto/get-object storage (:media-id row3))))
+ (t/is (some? (sto/get-object storage (:media-id row3)))))
- )))
+ ))
(t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*)
@@ -269,7 +269,7 @@
:object-id "test-key-1"
:data "data:base64,1234123124"}
- data2 {::th/type :upsert-file-object-thumbnail
+ data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile)
:file-id (:id file)
:object-id "test-key-2"
diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs
index 794865923..0ad8ba02e 100644
--- a/frontend/src/app/main/data/dashboard.cljs
+++ b/frontend/src/app/main/data/dashboard.cljs
@@ -137,7 +137,7 @@
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)]
- (->> (rp/command! :get-webhooks {:team-id team-id})
+ (->> (rp/cmd! :get-webhooks {:team-id team-id})
(rx/map team-webhooks-fetched))))))
;; --- EVENT: fetch-projects
@@ -302,7 +302,7 @@
(ptk/reify ::fetch-builtin-templates
ptk/WatchEvent
(watch [_ _ _]
- (->> (rp/command :retrieve-list-of-builtin-templates)
+ (->> (rp/cmd! :retrieve-list-of-builtin-templates)
(rx/map builtin-templates-fetched)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -555,7 +555,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/command! :delete-webhook params)
+ (->> (rp/cmd! :delete-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
@@ -578,7 +578,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/command! :update-webhook params)
+ (->> (rp/cmd! :update-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
@@ -598,7 +598,7 @@
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/command! :create-webhook params)
+ (->> (rp/cmd! :create-webhook params)
(rx/tap on-success)
(rx/catch on-error))))))
diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs
index 09eea1dda..1ad280bab 100644
--- a/frontend/src/app/main/data/exports.cljs
+++ b/frontend/src/app/main/data/exports.cljs
@@ -101,7 +101,7 @@
{:enabled true
:page-id page-id
:file-id file-id
- :object-id (:id frame)
+ :object-id (:id frame)
:shape frame
:name (:name frame)})]
@@ -145,7 +145,7 @@
ptk/WatchEvent
(watch [_ _ _]
(when (= status "ended")
- (->> (rp/command! :export {:cmd :get-resource :blob? true :id resource-id})
+ (->> (rp/cmd! :export {:cmd :get-resource :blob? true :id resource-id})
(rx/delay 500)
(rx/map #(dom/trigger-download filename %)))))))
@@ -165,9 +165,9 @@
:wait true}]
(rx/concat
(rx/of ::dwp/force-persist)
- (->> (rp/command! :export params)
+ (->> (rp/cmd! :export params)
(rx/mapcat (fn [{:keys [id filename]}]
- (->> (rp/command! :export {:cmd :get-resource :blob? true :id id})
+ (->> (rp/cmd! :export {:cmd :get-resource :blob? true :id id})
(rx/map (fn [data]
(dom/trigger-download filename data)
(clear-export-state uuid/zero))))))
@@ -213,7 +213,7 @@
;; Launch the exportation process and stores the resource id
;; locally.
- (->> (rp/command! :export params)
+ (->> (rp/cmd! :export params)
(rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id)
(initialize-export-status exports cmd resource))))
diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs
index 5f172dfc1..9d9bf0ccf 100644
--- a/frontend/src/app/main/data/users.cljs
+++ b/frontend/src/app/main/data/users.cljs
@@ -535,7 +535,7 @@
(ptk/reify ::fetch-access-tokens
ptk/WatchEvent
(watch [_ _ _]
- (->> (rp/command! :get-access-tokens)
+ (->> (rp/cmd! :get-access-tokens)
(rx/map access-tokens-fetched)))))
;; --- EVENT: create-access-token
@@ -555,7 +555,7 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/command! :create-access-token params)
+ (->> (rp/cmd! :create-access-token params)
(rx/map access-token-created)
(rx/tap on-success)
(rx/catch on-error))))))
@@ -571,6 +571,6 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
- (->> (rp/command! :delete-access-token params)
+ (->> (rp/cmd! :delete-access-token params)
(rx/tap on-success)
(rx/catch on-error))))))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index f7a94eac2..467dec79a 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -267,6 +267,29 @@
(when needs-update?
(rx/of (dwl/notify-sync-file file-id)))))))
+(defn- fetch-thumbnail-blob-uri
+ [uri]
+ (->> (http/send! {:uri uri
+ :response-type :blob
+ :method :get})
+ (rx/map :body)
+ (rx/map (fn [blob] (.createObjectURL js/URL blob)))))
+
+(defn- fetch-thumbnail-blobs
+ [file-id]
+ (->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
+ (rx/mapcat (fn [thumbnails]
+ (->> (rx/from thumbnails)
+ (rx/mapcat (fn [[k v]]
+ ;; we only need to fetch the thumbnail if
+ ;; it is a data:uri, otherwise we can just
+ ;; use the value as is.
+ (if (.startsWith v "data:")
+ (->> (fetch-thumbnail-blob-uri v)
+ (rx/map (fn [uri] [k uri])))
+ (rx/of [k v])))))))
+ (rx/reduce conj {})))
+
(defn- fetch-bundle
[project-id file-id]
(ptk/reify ::fetch-bundle
@@ -285,9 +308,8 @@
;; WTF is this?
share-id (-> state :viewer-local :share-id)
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
-
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
- (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
+ (fetch-thumbnail-blobs file-id)
(rp/cmd! :get-project {:id project-id})
(rp/cmd! :get-team-users {:file-id file-id})
(rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))
diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs
index 266afb7f4..424899ad8 100644
--- a/frontend/src/app/main/data/workspace/media.cljs
+++ b/frontend/src/app/main/data/workspace/media.cljs
@@ -94,7 +94,7 @@
(->> (rx/from uris)
(rx/filter (comp not svg-url?))
(rx/map prepare)
- (rx/mapcat #(rp/command! :create-file-media-object-from-url %))
+ (rx/mapcat #(rp/cmd! :create-file-media-object-from-url %))
(rx/do on-image))
(->> (rx/from uris)
diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs
index 87b761305..8045bfc33 100644
--- a/frontend/src/app/main/data/workspace/svg_upload.cljs
+++ b/frontend/src/app/main/data/workspace/svg_upload.cljs
@@ -478,10 +478,10 @@
:content (data-uri->blob uri)}
{:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data]
- (->> (rp/command! (if (contains? uri-data :content)
- :upload-file-media-object
- :create-file-media-object-from-url)
- uri-data)
+ (->> (rp/cmd! (if (contains? uri-data :content)
+ :upload-file-media-object
+ :create-file-media-object-from-url)
+ uri-data)
;; When the image uploaded fail we skip the shape
;; returning `nil` will afterward not create the shape.
(rx/catch #(rx/of nil))
diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs
index b4cdeda71..64f1b4c03 100644
--- a/frontend/src/app/main/data/workspace/thumbnails.cljs
+++ b/frontend/src/app/main/data/workspace/thumbnails.cljs
@@ -14,8 +14,10 @@
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
+ [app.main.worker :as uw]
[app.util.dom :as dom]
- [app.util.timers :as ts]
+ [app.util.http :as http]
+ [app.util.timers :as tm]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[potok.core :as ptk]))
@@ -29,28 +31,22 @@
(rx/filter #(= % id))
(rx/take 1)))
-(defn thumbnail-canvas-blob-stream
+(defn get-thumbnail
[object-id]
;; Look for the thumbnail canvas to send the data to the backend
- (let [node (dom/query (dm/fmt "canvas.thumbnail-canvas[data-object-id='%'][data-ready='true']" object-id))
+ (let [node (dom/query (dm/fmt "image.thumbnail-canvas[data-object-id='%'][data-ready='true']" object-id))
stopper (->> st/stream
(rx/filter (ptk/type? :app.main.data.workspace/finalize-page))
(rx/take 1))]
+ ;; renders #svg image
(if (some? node)
- ;; Success: we generate the blob (async call)
- (rx/create
- (fn [subs]
- (ts/raf
- (fn []
- (.toBlob node (fn [blob]
- (rx/push! subs blob)
- #_(rx/end! subs))
- "image/png")))
- (constantly nil)))
+ (->> (rx/from (js/createImageBitmap node))
+ (rx/switch-map #(uw/ask! {:cmd :thumbnails/render-offscreen-canvas} %))
+ (rx/map :result))
;; Not found, we retry after delay
(->> (rx/timer 250)
- (rx/flat-map #(thumbnail-canvas-blob-stream object-id))
+ (rx/merge-map (partial get-thumbnail object-id))
(rx/take-until stopper)))))
(defn clear-thumbnail
@@ -59,8 +55,33 @@
ptk/UpdateEvent
(update [_ state]
(let [object-id (dm/str page-id frame-id)]
+ (when-let [uri (dm/get-in state [:workspace-thumbnails object-id])]
+ (tm/schedule-on-idle (partial wapi/revoke-uri uri)))
(update state :workspace-thumbnails dissoc object-id)))))
+(defn set-workspace-thumbnail
+ [object-id uri]
+ (let [prev-uri* (volatile! nil)]
+ (ptk/reify ::set-workspace-thumbnail
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [prev-uri (dm/get-in state [:workspace-thumbnails object-id])]
+ (some->> prev-uri (vreset! prev-uri*))
+ (update state :workspace-thumbnails assoc object-id uri)))
+
+ ptk/EffectEvent
+ (effect [_ _ _]
+ (tm/schedule-on-idle #(some-> ^boolean @prev-uri* wapi/revoke-uri))))))
+
+(defn duplicate-thumbnail
+ [old-id new-id]
+ (ptk/reify ::duplicate-thumbnail
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [page-id (:current-page-id state)
+ thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])]
+ (update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail)))))
+
(defn update-thumbnail
"Updates the thumbnail information for the given frame `id`"
([page-id frame-id]
@@ -70,39 +91,33 @@
(ptk/reify ::update-thumbnail
ptk/WatchEvent
(watch [_ state _]
- (let [object-id (dm/str page-id frame-id)
- file-id (or file-id (:current-file-id state))
- blob-result (thumbnail-canvas-blob-stream object-id)
- params {:file-id file-id :object-id object-id :data nil}]
+ (let [object-id (dm/str page-id frame-id)
+ file-id (or file-id (:current-file-id state))]
(rx/concat
;; Delete the thumbnail first so if we interrupt we can regenerate after
- (->> (rp/cmd! :upsert-file-object-thumbnail params)
- (rx/catch #(rx/empty)))
-
- ;; Remove the thumbnail temporary. If the user changes pages the thumbnail is regenerated
- (rx/of #(update % :workspace-thumbnails assoc object-id nil))
+ (->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id})
+ (rx/catch rx/empty))
;; Send the update to the back-end
- (->> blob-result
+ (->> (get-thumbnail object-id)
+ (rx/filter (fn [data] (and (some? data) (some? file-id))))
(rx/merge-map
- (fn [blob]
- (if (some? blob)
- (wapi/read-file-as-data-url blob)
- (rx/of nil))))
+ (fn [uri]
+ (rx/merge
+ (rx/of (set-workspace-thumbnail object-id uri))
- (rx/merge-map
- (fn [data]
- (if (and (some? data) (some? file-id))
- (let [params (assoc params :data data)]
- (rx/merge
- ;; Update the local copy of the thumbnails so we don't need to request it again
- (rx/of #(update % :workspace-thumbnails assoc object-id data))
- (->> (rp/cmd! :upsert-file-object-thumbnail params)
- (rx/catch #(rx/empty))
- (rx/ignore))))
+ (->> (http/send! {:uri uri :response-type :blob :method :get})
+ (rx/map :body)
+ (rx/mapcat (fn [blob]
+ ;; Send the data to backend
+ (let [params {:file-id file-id
+ :object-id object-id
+ :media blob}]
+ (rp/cmd! :create-file-object-thumbnail params))))
+ (rx/catch rx/empty)
+ (rx/ignore)))))
- (rx/empty))))
(rx/catch #(do (.error js/console %)
(rx/empty))))))))))
@@ -137,6 +152,7 @@
(and new-frame-id (not= uuid/zero new-frame-id))
(conj [page-id new-frame-id]))))]
+
(into #{}
(comp (mapcat extract-ids)
(mapcat get-frame-id))
@@ -154,7 +170,7 @@
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
(= ::watch-state-changes (ptk/type %)))))
- workspace-data-str
+ workspace-data-s
(->> (rx/concat
(rx/of nil)
(rx/from-atom refs/workspace-data {:emit-current-value? true}))
@@ -162,33 +178,24 @@
;; deleted objects
(rx/buffer 2 1))
- change-str
+ change-s
(->> stream
(rx/filter #(or (dch/commit-changes? %)
(= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change)))
(rx/observe-on :async))
- frame-changes-str
- (->> change-str
- (rx/with-latest-from workspace-data-str)
+ frame-changes-s
+ (->> change-s
+ (rx/with-latest-from workspace-data-s)
(rx/flat-map extract-frame-changes)
(rx/share))]
(->> (rx/merge
- (->> frame-changes-str
+ (->> frame-changes-s
(rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state))))
(rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id))))
- (->> frame-changes-str
+ (->> frame-changes-s
(rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state))))
(rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id)))))
(rx/take-until stopper))))))
-
-(defn duplicate-thumbnail
- [old-id new-id]
- (ptk/reify ::duplicate-thumbnail
- ptk/UpdateEvent
- (update [_ state]
- (let [page-id (:current-page-id state)
- thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])]
- (update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail)))))
diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs
index 68f15b9e9..ca834ce20 100644
--- a/frontend/src/app/main/repo.cljs
+++ b/frontend/src/app/main/repo.cljs
@@ -10,29 +10,8 @@
[app.common.uri :as u]
[app.config :as cf]
[app.util.http :as http]
- [beicon.core :as rx]))
-
-(derive :get-all-projects ::query)
-(derive :get-comment-threads ::query)
-(derive :get-file ::query)
-(derive :get-file-fragment ::query)
-(derive :get-file-libraries ::query)
-(derive :get-file-object-thumbnails ::query)
-(derive :get-font-variants ::query)
-(derive :get-profile ::query)
-(derive :get-project ::query)
-(derive :get-projects ::query)
-(derive :get-team-invitations ::query)
-(derive :get-team-members ::query)
-(derive :get-team-shared-files ::query)
-(derive :get-team-stats ::query)
-(derive :get-team-users ::query)
-(derive :get-teams ::query)
-(derive :get-view-only-bundle ::query)
-(derive :search-files ::query)
-(derive :retrieve-list-of-builtin-templates ::query)
-(derive :get-unread-comment-threads ::query)
-(derive :get-team-recent-files ::query)
+ [beicon.core :as rx]
+ [cuerdas.core :as str]))
(defn handle-response
[{:keys [status body] :as response}]
@@ -65,120 +44,66 @@
:status status
:data body})))
-(defn- send-query!
- "A simple helper for send and receive transit data on the penpot
- query api."
- ([id params]
- (send-query! id params nil))
- ([id params {:keys [raw-transit?]}]
- (let [decode-transit (if raw-transit?
- http/conditional-error-decode-transit
- http/conditional-decode-transit)]
- (->> (http/send! {:method :get
- :uri (u/join @cf/public-uri "api/rpc/query/" (name id))
- :headers {"accept" "application/transit+json"}
- :credentials "include"
- :query params})
- (rx/map decode-transit)
- (rx/mapcat handle-response)))))
+(def default-options
+ {:update-file {:query-params [:id]}
+ :get-raw-file {:rename-to :get-file :raw-transit? true}
+ :upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
+ :create-file-object-thumbnail {:query-params [:file-id :object-id]
+ :form-data? true}
+ :export-binfile {:response-type :blob}
+ :import-binfile {:form-data? true}
+ :retrieve-list-of-builtin-templates {:query-params :all}
+ })
-(defn- send-mutation!
+(defn- send!
"A simple helper for a common case of sending and receiving transit
data to the penpot mutation api."
- [id params]
- (->> (http/send! {:method :post
- :uri (u/join @cf/public-uri "api/rpc/mutation/" (name id))
- :headers {"accept" "application/transit+json"}
- :credentials "include"
- :body (http/transit-data params)})
- (rx/map http/conditional-decode-transit)
- (rx/mapcat handle-response)))
+ [id params options]
+ (let [{:keys [response-type
+ form-data?
+ raw-transit?
+ query-params
+ rename-to]}
+ (-> (get default-options id)
+ (merge options))
-(defn- send-command!
- "A simple helper for a common case of sending and receiving transit
- data to the penpot mutation api."
- [id params {:keys [response-type form-data? raw-transit? forward-query-params]}]
- (let [decode-fn (if raw-transit?
+ decode-fn (if raw-transit?
http/conditional-error-decode-transit
http/conditional-decode-transit)
- method (if (isa? id ::query) :get :post)]
- (->> (http/send! {:method method
- :uri (u/join @cf/public-uri "api/rpc/command/" (name id))
- :credentials "include"
- :headers {"accept" "application/transit+json"}
- :body (when (= method :post)
- (if form-data?
- (http/form-data params)
- (http/transit-data params)))
- :query (if (= method :get)
- params
- (if forward-query-params
- (select-keys params forward-query-params)
- nil))
- :response-type (or response-type :text)})
+ id (or rename-to id)
+ nid (name id)
+ method (cond
+ (= query-params :all) :get
+ (str/starts-with? nid "get-") :get
+ :else :post)
+
+ request {:method method
+ :uri (u/join @cf/public-uri "api/rpc/command/" (name id))
+ :credentials "include"
+ :headers {"accept" "application/transit+json"}
+ :body (when (= method :post)
+ (if form-data?
+ (http/form-data params)
+ (http/transit-data params)))
+ :query (if (= method :get)
+ params
+ (if query-params
+ (select-keys params query-params)
+ nil))
+ :response-type (or response-type :text)}]
+
+ (->> (http/send! request)
(rx/map decode-fn)
(rx/mapcat handle-response))))
-(defn- dispatch [& args] (first args))
+(defmulti cmd! (fn [id _] id))
-(defmulti query dispatch)
-(defmulti mutation dispatch)
-(defmulti command dispatch)
-
-(defmethod query :default
+(defmethod cmd! :default
[id params]
- (send-query! id params))
+ (send! id params nil))
-(defmethod command :get-raw-file
- [_id params]
- (send-command! :get-file params {:raw-transit? true}))
-
-(defmethod mutation :default
- [id params]
- (send-mutation! id params))
-
-(defmethod command :default
- [id params]
- (send-command! id params nil))
-
-(defmethod command :update-file
- [id params]
- (send-command! id params {:forward-query-params [:id]}))
-
-(defmethod command :upsert-file-object-thumbnail
- [id params]
- (send-command! id params {:forward-query-params [:file-id :object-id]}))
-
-(defmethod command :get-file-object-thumbnails
- [id params]
- (send-command! id params {:forward-query-params [:file-id]}))
-
-(defmethod command :export-binfile
- [id params]
- (send-command! id params {:response-type :blob}))
-
-(defmethod command :import-binfile
- [id params]
- (send-command! id params {:form-data? true}))
-
-(defn query!
- ([id] (query id {}))
- ([id params] (query id params)))
-
-(defn mutation!
- ([id] (mutation id {}))
- ([id params] (mutation id params)))
-
-(defn command!
- ([id] (command id {}))
- ([id params] (command id params)))
-
-(defn cmd!
- ([id] (command id {}))
- ([id params] (command id params)))
-
-(defmethod command :login-with-oidc
+(defmethod cmd! :login-with-oidc
[_ {:keys [provider] :as params}]
(let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)]
@@ -199,7 +124,7 @@
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
-(defmethod command :export
+(defmethod cmd! :export
[_ params]
(let [default {:wait false :blob? false}]
(send-export (merge default params))))
@@ -208,16 +133,7 @@
(derive :update-profile-photo ::multipart-upload)
(derive :update-team-photo ::multipart-upload)
-(defmethod mutation ::multipart-upload
- [id params]
- (->> (http/send! {:method :post
- :uri (u/join @cf/public-uri "api/rpc/mutation/" (name id))
- :credentials "include"
- :body (http/form-data params)})
- (rx/map http/conditional-decode-transit)
- (rx/mapcat handle-response)))
-
-(defmethod command ::multipart-upload
+(defmethod cmd! ::multipart-upload
[id params]
(->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/command/" (name id))
diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs
index 18a6405bf..af9d206a0 100644
--- a/frontend/src/app/main/ui/auth/login.cljs
+++ b/frontend/src/app/main/ui/auth/login.cljs
@@ -37,7 +37,7 @@
(defn- login-with-oidc
[event provider params]
(dom/prevent-default event)
- (->> (rp/command! :login-with-oidc (assoc params :provider provider))
+ (->> (rp/cmd! :login-with-oidc (assoc params :provider provider))
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(if redirect-uri
(.replace js/location redirect-uri)
@@ -57,7 +57,7 @@
(dom/prevent-default event)
(dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)]
- (->> (rp/command! :login-with-ldap params)
+ (->> (rp/cmd! :login-with-ldap params)
(rx/subs (fn [profile]
(if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs
index bfb3dca09..114b36e0d 100644
--- a/frontend/src/app/main/ui/auth/register.cljs
+++ b/frontend/src/app/main/ui/auth/register.cljs
@@ -101,7 +101,7 @@
(fn [form _event]
(reset! submitted? true)
(let [cdata (:clean-data @form)]
- (->> (rp/command! :prepare-register-profile cdata)
+ (->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/finalize #(reset! submitted? false))
(rx/subs
@@ -232,7 +232,7 @@
(fn [form _event]
(reset! submitted? true)
(let [params (:clean-data @form)]
- (->> (rp/command! :register-profile params)
+ (->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false))
(rx/subs on-success
(partial handle-register-error form))))))]
diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs
index 5a4fcbc67..7534d3731 100644
--- a/frontend/src/app/main/ui/auth/verify_token.cljs
+++ b/frontend/src/app/main/ui/auth/verify_token.cljs
@@ -65,7 +65,7 @@
(mf/with-effect []
(dom/set-html-title (tr "title.default"))
- (->> (rp/command! :verify-token {:token token})
+ (->> (rp/cmd! :verify-token {:token token})
(rx/subs
(fn [tdata]
(handle-token tdata))
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index f79b2d4b6..0a86ec2a7 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -177,7 +177,7 @@
(->> (rx/from files)
(rx/flat-map
(fn [file]
- (->> (rp/command :has-file-libraries {:file-id (:id file)})
+ (->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
(rx/map #(assoc file :has-libraries? %)))))
(rx/reduce conj [])
(rx/subs
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 73e21d264..706b8774b 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -46,7 +46,8 @@
(let [features (cond-> ffeat/enabled
(features/active-feature? :components-v2)
(conj "components/v2"))]
- (wrk/ask! {:cmd :thumbnails/generate
+
+ (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn (:revn file)
:file-id (:id file)
:file-name (:name file)
diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs
index 2b718d916..864e6b2d4 100644
--- a/frontend/src/app/main/ui/routes.cljs
+++ b/frontend/src/app/main/ui/routes.cljs
@@ -56,7 +56,7 @@
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
-
+
["/debug/components-preview" :debug-components-preview]
;; Used for export
@@ -98,7 +98,7 @@
;; We just recheck with an additional profile request; this avoids
;; some race conditions that causes unexpected redirects on
;; invitations workflows (and probably other cases).
- (->> (rp/command! :get-profile)
+ (->> (rp/cmd! :get-profile)
(rx/subs (fn [{:keys [id] :as profile}]
(if (= id uuid/zero)
(st/emit! (rt/nav :auth-login))
diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs
index e198088e6..fd3d9eedb 100644
--- a/frontend/src/app/main/ui/settings/feedback.cljs
+++ b/frontend/src/app/main/ui/settings/feedback.cljs
@@ -55,7 +55,7 @@
(fn [form _]
(reset! loading true)
(let [data (:clean-data @form)]
- (->> (rp/command! :send-user-feedback data)
+ (->> (rp/cmd! :send-user-feedback data)
(rx/subs on-succes on-error)))))]
[:& fm/form {:class "feedback-form"
diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs
index 11be6ba00..a8e4a43b1 100644
--- a/frontend/src/app/main/ui/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/shapes/frame.cljs
@@ -88,36 +88,41 @@
(mf/defc frame-thumbnail-image
{::mf/wrap-props false}
[props]
+ (let [shape (unchecked-get props "shape")
+ bounds (or (unchecked-get props "bounds")
+ (gsh/points->selrect (:points shape)))
- (let [shape (obj/get props "shape")
- bounds (or (obj/get props "bounds") (gsh/points->selrect (:points shape)))]
+ shape-id (:id shape)
+ thumb (:thumbnail shape)
- (when (:thumbnail shape)
- [:*
- [:image.frame-thumbnail
- {:id (dm/str "thumbnail-" (:id shape))
- :href (:thumbnail shape)
- :x (:x bounds)
- :y (:y bounds)
- :width (:width bounds)
- :height (:height bounds)
- ;; DEBUG
- :style {:filter (when (and (not (cf/check-browser? :safari))(debug? :thumbnails)) "sepia(1)")}}]
+ debug? (debug? :thumbnails)
+ safari? (cf/check-browser? :safari)]
- ;; Safari don't support filters so instead we add a rectangle around the thumbnail
- (when (and (cf/check-browser? :safari) (debug? :thumbnails))
- [:rect {:x (+ (:x bounds) 4)
- :y (+ (:y bounds) 4)
- :width (- (:width bounds) 8)
- :height (- (:height bounds) 8)
- :stroke "red"
- :stroke-width 2}])])))
+ [:*
+ [:image.frame-thumbnail
+ {:id (dm/str "thumbnail-" shape-id)
+ :href thumb
+ :decoding "async"
+ :x (:x bounds)
+ :y (:y bounds)
+ :width (:width bounds)
+ :height (:height bounds)
+ :style {:filter (when (and (not ^boolean safari?) ^boolean debug?) "sepia(1)")}}]
+
+ ;; Safari don't support filters so instead we add a rectangle around the thumbnail
+ (when (and ^boolean safari? ^boolean debug?)
+ [:rect {:x (+ (:x bounds) 4)
+ :y (+ (:y bounds) 4)
+ :width (- (:width bounds) 8)
+ :height (- (:height bounds) 8)
+ :stroke "red"
+ :stroke-width 2}])]))
(mf/defc frame-thumbnail
{::mf/wrap-props false}
[props]
- (let [shape (obj/get props "shape")]
- (when (:thumbnail shape)
+ (let [shape (unchecked-get props "shape")]
+ (when ^boolean (:thumbnail shape)
[:> frame-container props
[:> frame-thumbnail-image props]])))
diff --git a/frontend/src/app/main/ui/workspace/header.cljs b/frontend/src/app/main/ui/workspace/header.cljs
index 7be23d60d..8dc730b31 100644
--- a/frontend/src/app/main/ui/workspace/header.cljs
+++ b/frontend/src/app/main/ui/workspace/header.cljs
@@ -179,7 +179,7 @@
(->> (rx/of file)
(rx/flat-map
(fn [file]
- (->> (rp/command :has-file-libraries {:file-id (:id file)})
+ (->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
(rx/map #(assoc file :has-libraries? %)))))
(rx/reduce conj [])
(rx/subs
diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
index fd11a1e48..b18e6b547 100644
--- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs
@@ -11,7 +11,6 @@
[app.common.pages.helpers :as cph]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt]
- [app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
@@ -79,72 +78,70 @@
::mf/wrap-props false}
[props]
- (let [shape (unchecked-get props "shape")
- frame-id (:id shape)
- objects (wsh/lookup-page-objects @st/state)
- node-ref (mf/use-var nil)
- modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id))
- modifiers (mf/deref modifiers-ref)]
+ (let [shape (unchecked-get props "shape")
+ thumbnail? (unchecked-get props "thumbnail?")
- (fdm/use-dynamic-modifiers objects @node-ref modifiers)
+ page-id (mf/use-ctx ctx/current-page-id)
+ frame-id (:id shape)
- (let [thumbnail? (unchecked-get props "thumbnail?")
- fonts (mf/use-memo (mf/deps shape objects) #(ff/shape->fonts shape objects))
- fonts (-> fonts (hooks/use-equal-memo))
+ objects (wsh/lookup-page-objects @st/state)
- force-render (mf/use-state false)
+ node* (mf/use-var nil)
+ force-render* (mf/use-state false)
+ force-render? (deref force-render*)
- ;; Thumbnail data
- page-id (mf/use-ctx ctx/current-page-id)
+ ;; when `true` we've called the mount for the frame
+ rendered* (mf/use-var false)
- ;; when `true` we've called the mount for the frame
- rendered? (mf/use-var false)
+ modifiers-ref (mf/with-memo [frame-id]
+ (refs/workspace-modifiers-by-frame-id frame-id))
+ modifiers (mf/deref modifiers-ref)
- disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers]))
- [on-load-frame-dom render-frame? thumbnail-renderer]
- (ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render)
+ fonts (mf/with-memo [shape objects]
+ (ff/shape->fonts shape objects))
+ fonts (hooks/use-equal-memo fonts)
- on-frame-load
- (fns/use-node-store thumbnail? node-ref rendered? render-frame?)]
+ disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers]))
- (mf/use-effect
- (mf/deps fonts)
- (fn []
- (->> (rx/from fonts)
- (rx/merge-map fonts/fetch-font-css)
- (rx/ignore))))
+ [on-load-frame-dom render-frame? thumbnail-renderer]
+ (ftr/use-render-thumbnail page-id shape node* rendered* disable-thumbnail? force-render?)
- (mf/use-effect
- (fn []
- ;; When a change in the data is received a "force-render" event is emitted
- ;; that will force the component to be mounted in memory
- (let [sub
- (->> (dwt/force-render-stream (:id shape))
- (rx/take-while #(not @rendered?))
- (rx/subs #(reset! force-render true)))]
- #(when sub
- (rx/dispose! sub)))))
+ on-frame-load
+ (fns/use-node-store thumbnail? node* rendered* render-frame?)]
- (mf/use-effect
- (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?)
- (fn []
- (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?))
- (mf/mount
- (mf/element frame-shape
- #js {:ref on-load-frame-dom :shape shape :fonts fonts})
+ (fdm/use-dynamic-modifiers objects @node* modifiers)
- @node-ref)
- (when (not @rendered?) (reset! rendered? true)))))
+ (mf/with-effect []
+ ;; When a change in the data is received a "force-render" event is emitted
+ ;; that will force the component to be mounted in memory
+ (let [sub (->> (dwt/force-render-stream frame-id)
+ (rx/take-while #(not @rendered*))
+ (rx/subs #(reset! force-render* true)))]
+ #(some-> sub rx/dispose!)))
+
+ (mf/with-effect [shape fonts thumbnail? on-load-frame-dom force-render? render-frame?]
+ (when (and (some? @node*)
+ (or @rendered*
+ (not thumbnail?)
+ force-render?
+ render-frame?))
+ (let [elem (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts})]
+ (mf/mount elem @node*)
+ (when (not @rendered*)
+ (reset! rendered* true)))))
+
+ [:& shape-container {:shape shape}
+ [:g.frame-container {:id (dm/str "frame-container-" frame-id)
+ :key "frame-container"
+ :ref on-frame-load
+ :opacity (when (:hidden shape) 0)}
+ [:& ff/fontfaces-style {:fonts fonts}]
+ [:g.frame-thumbnail-wrapper
+ {:id (dm/str "thumbnail-container-" frame-id)
+ ;; Hide the thumbnail when not displaying
+ :opacity (when-not thumbnail? 0)}
+ thumbnail-renderer]]
+
+ ]))))
- [:& shape-container {:shape shape}
- [:g.frame-container {:id (dm/str "frame-container-" (:id shape))
- :key "frame-container"
- :ref on-frame-load
- :opacity (when (:hidden shape) 0)}
- [:& ff/fontfaces-style {:fonts fonts}]
- [:g.frame-thumbnail-wrapper
- {:id (dm/str "thumbnail-container-" (:id shape))
- ;; Hide the thumbnail when not displaying
- :opacity (when-not thumbnail? 0)}
- thumbnail-renderer]]])))))
diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs
index 697998f20..8dbf52544 100644
--- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs
@@ -22,27 +22,8 @@
[beicon.core :as rx]
[cuerdas.core :as str]
[debug :refer [debug?]]
- [promesa.core :as p]
[rumext.v2 :as mf]))
-(defn- draw-thumbnail-canvas!
- [canvas-node img-node]
- (try
- (when (and (some? canvas-node) (some? img-node))
- (let [canvas-context (.getContext canvas-node "2d")
- canvas-width (.-width canvas-node)
- canvas-height (.-height canvas-node)]
- (.clearRect canvas-context 0 0 canvas-width canvas-height)
- (.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
-
- ;; Set a true on the next animation frame, we make sure the drawImage is completed
- (ts/raf
- #(dom/set-data! canvas-node "ready" "true"))
- true))
- (catch :default err
- (.error js/console err)
- false)))
-
(defn- remove-image-loading
"Remove the changes related to change a url for its embed value. This is necessary
so we don't have to recalculate the thumbnail when the image loads."
@@ -57,19 +38,39 @@
(str/starts-with? (.-oldValue change) "http"))))))
[value]))
+(defn- create-svg-blob-uri-from
+ [fixed-width fixed-height rect node style-node]
+ (let [{:keys [x y width height]} rect
+ viewbox (dm/str x " " y " " width " " height)
+
+ ;; This is way faster than creating a node
+ ;; through the DOM API
+ svg-data
+ (dm/fmt ""
+ viewbox
+ fixed-width
+ fixed-height
+ (if (some? style-node) (dom/node->xml style-node) "")
+ (dom/node->xml node))
+
+ ;; create SVG blob
+ blob (js/Blob. #js [svg-data] #js {:type "image/svg+xml;charset=utf-8"})
+ url (dm/str (.createObjectURL js/URL blob) "#svg")]
+ ;; returns the url and the node
+ url))
+
(defn use-render-thumbnail
- "Hook that will create the thumbnail thata"
+ "Hook that will create the thumbnail data"
[page-id {:keys [id] :as shape} node-ref rendered? disable? force-render]
- (let [frame-canvas-ref (mf/use-ref nil)
- frame-image-ref (mf/use-ref nil)
+ (let [frame-image-ref (mf/use-ref nil)
- disable-ref? (mf/use-var disable?)
+ disable* (mf/use-var disable?)
+ regenerate* (mf/use-var false)
- regenerate-thumbnail (mf/use-var false)
-
- all-children-ref (mf/use-memo (mf/deps id) #(refs/all-children-objects id))
- all-children (mf/deref all-children-ref)
+ all-children-ref (mf/with-memo [id]
+ (refs/all-children-objects id))
+ all-children (mf/deref all-children-ref)
{:keys [x y width height] :as shape-bb}
(if (:show-content shape)
@@ -83,51 +84,46 @@
[(/ (* width (mth/clamp height 250 2000)) height)
(mth/clamp height 250 2000)])
- image-url (mf/use-state nil)
- observer-ref (mf/use-var nil)
+ svg-uri* (mf/use-state nil)
+ bitmap-uri* (mf/use-state nil)
+ observer* (mf/use-var nil)
- shape-bb-ref (hooks/use-update-var shape-bb)
+ shape-bb* (hooks/use-update-var shape-bb)
+ updates-s (mf/use-memo rx/subject)
- updates-str (mf/use-memo #(rx/subject))
-
- thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id))
- thumbnail-data (mf/deref thumbnail-data-ref)
-
- ;; We only need the zoom level in Safari. For other browsers we don't want to activate this because
- ;; will render for every zoom change
- zoom (when (cf/check-browser? :safari) (mf/deref refs/selected-zoom))
-
- prev-thumbnail-data (hooks/use-previous thumbnail-data)
+ thumbnail-uri-ref (mf/with-memo [page-id id]
+ (refs/thumbnail-frame-data page-id id))
+ thumbnail-uri (mf/deref thumbnail-uri-ref)
;; State to indicate to the parent that should render the frame
- render-frame? (mf/use-state (not thumbnail-data))
+ render-frame* (mf/use-state (not thumbnail-uri))
+ debug? (debug? :thumbnails)
- ;; State variable to select whether we show the image thumbnail or the canvas thumbnail
- show-frame-thumbnail (mf/use-state (some? thumbnail-data))
- disable-fills? (or @show-frame-thumbnail (some? @image-url))
-
- on-image-load
- (mf/use-callback
- (mf/deps @show-frame-thumbnail)
+ on-bitmap-load
+ (mf/use-fn
(fn []
- (let [canvas-node (mf/ref-val frame-canvas-ref)
- img-node (mf/ref-val frame-image-ref)]
- (when (draw-thumbnail-canvas! canvas-node img-node)
- (when-not (cf/check-browser? :safari)
- (reset! image-url nil))
+ ;; We revoke the SVG Blob URI to free memory only when we
+ ;; are sure that it is not used anymore.
+ (wapi/revoke-uri @svg-uri*)
+ (reset! svg-uri* nil)))
- (when @show-frame-thumbnail
- (reset! show-frame-thumbnail false))
- ;; If we don't have the thumbnail data saved (normally the first load) we update the data
- ;; when available
- (when (not @thumbnail-data-ref)
- (st/emit! (dwt/update-thumbnail page-id id) ))
+ on-svg-load
+ (mf/use-callback
+ (fn []
+ (let [image-node (mf/ref-val frame-image-ref)]
+ (dom/set-data! image-node "ready" "true")
- (reset! render-frame? false)))))
+ ;; If we don't have the thumbnail data saved (normally the first load) we update the data
+ ;; when available
+ (when (not ^boolean @thumbnail-uri-ref)
+ (st/emit! (dwt/update-thumbnail page-id id)))
+
+ (reset! render-frame* false))))
generate-thumbnail
- (mf/use-callback
+ (mf/use-fn
+ (mf/deps id)
(fn generate-thumbnail []
(try
;; When starting generating the canvas we mark it as not ready so its not send to back until
@@ -135,158 +131,106 @@
(let [node @node-ref]
(if (dom/has-children? node)
;; The frame-content need to have children in order to generate the thumbnail
- (let [style-node (dom/query (dm/str "#frame-container-" (:id shape) " style"))
-
- {:keys [x y width height]} @shape-bb-ref
- viewbox (dm/str x " " y " " width " " height)
-
- ;; This is way faster than creating a node through the DOM API
- svg-data
- (dm/fmt ""
- viewbox
- width
- height
- (if (some? style-node) (dom/node->xml style-node) "")
- (dom/node->xml node))
-
- blob (js/Blob. #js [svg-data] #js {:type "image/svg+xml;charset=utf-8"})
-
- img-src (.createObjectURL js/URL blob)]
- (reset! image-url img-src))
+ (let [style-node (dom/query (dm/str "#frame-container-" id " style"))
+ url (create-svg-blob-uri-from fixed-width fixed-height @shape-bb* node style-node)]
+ (reset! svg-uri* url))
;; Node not yet ready, we schedule a new generation
- (ts/schedule generate-thumbnail)))
+ (ts/raf generate-thumbnail)))
(catch :default e
(.error js/console e)))))
on-change-frame
- (mf/use-callback
+ (mf/use-fn
+ (mf/deps id)
(fn []
- (when (and (some? @node-ref) @rendered? @regenerate-thumbnail)
+ (when (and ^boolean @node-ref
+ ^boolean @rendered?
+ ^boolean @regenerate*)
(let [loading-images? (some? (dom/query @node-ref "[data-loading='true']"))
- loading-fonts? (some? (dom/query (dm/str "#frame-container-" (:id shape) " > style[data-loading='true']")))]
- (when (and (not loading-images?) (not loading-fonts?))
+ loading-fonts? (some? (dom/query (dm/str "#frame-container-" id " > style[data-loading='true']")))]
+ (when (and (not loading-images?)
+ (not loading-fonts?))
(generate-thumbnail)
- (reset! regenerate-thumbnail false))))))
+ (reset! regenerate* false))))))
+ ;; When the frame is updated, it is marked as not ready
+ ;; so that it is not sent to the background until
+ ;; it is regenerated.
on-update-frame
- (mf/use-callback
+ (mf/use-fn
(fn []
- (let [canvas-node (mf/ref-val frame-canvas-ref)]
- (when (not= "false" (dom/get-data canvas-node "ready"))
- (dom/set-data! canvas-node "ready" "false")))
- (when (not @disable-ref?)
- (reset! render-frame? true)
- (reset! regenerate-thumbnail true))))
+ (let [image-node (mf/ref-val frame-image-ref)]
+ (when (not= "false" (dom/get-data image-node "ready"))
+ (dom/set-data! image-node "ready" "false")))
+ (when-not ^boolean @disable*
+ (reset! render-frame* true)
+ (reset! regenerate* true))))
on-load-frame-dom
- (mf/use-callback
+ (mf/use-fn
(fn [node]
- (when (and (some? node) (nil? @observer-ref))
- (when-not (some? @thumbnail-data-ref)
- (rx/push! updates-str :update))
+ (when (and (some? node)
+ (nil? @observer*))
+ (when-not (some? @thumbnail-uri-ref)
+ (rx/push! updates-s :update))
- (let [observer (js/MutationObserver. (partial rx/push! updates-str))]
+ (let [observer (js/MutationObserver. (partial rx/push! updates-s))]
(.observe observer node #js {:childList true :attributes true :attributeOldValue true :characterData true :subtree true})
- (reset! observer-ref observer)))))]
+ (reset! observer* observer)))))]
- (mf/use-effect
- (mf/deps thumbnail-data)
- (fn []
- (when (and (some? prev-thumbnail-data) (nil? thumbnail-data))
- (rx/push! updates-str :update))))
+ (mf/with-effect [thumbnail-uri]
+ (when (some? thumbnail-uri)
+ (reset! bitmap-uri* thumbnail-uri)))
- (mf/use-effect
- (mf/deps force-render)
- (fn []
- (when force-render
- (rx/push! updates-str :update))))
+ (mf/with-effect [force-render]
+ (when ^boolean force-render
+ (rx/push! updates-s :update)))
- (mf/use-effect
- (fn []
- (let [subid (->> updates-str
- (rx/map remove-image-loading)
- (rx/filter d/not-empty?)
- (rx/catch (fn [err] (.error js/console err)))
- (rx/subs on-update-frame))]
- #(rx/dispose! subid))))
+ (mf/with-effect []
+ (let [subid (->> updates-s
+ (rx/map remove-image-loading)
+ (rx/filter d/not-empty?)
+ (rx/catch (fn [err] (.error js/console err)))
+ (rx/subs on-update-frame))]
+ (partial rx/dispose! subid)))
;; on-change-frame will get every change in the frame
- (mf/use-effect
- (fn []
- (let [subid (->> updates-str
- (rx/debounce 400)
- (rx/observe-on :af)
- (rx/catch (fn [err] (.error js/console err)))
- (rx/subs on-change-frame))]
- #(rx/dispose! subid))))
+ (mf/with-effect []
+ (let [subid (->> updates-s
+ (rx/debounce 400)
+ (rx/observe-on :af)
+ (rx/catch (fn [err] (.error js/console err)))
+ (rx/subs on-change-frame))]
+ (partial rx/dispose! subid)))
- (mf/use-effect
- (mf/deps disable?)
- (fn []
- (when (and disable? (not @disable-ref?))
- (rx/push! updates-str :update))
- (reset! disable-ref? disable?)))
+ (mf/with-effect [disable?]
+ (when (and ^boolean disable?
+ (not @disable*))
+ (rx/push! updates-s :update))
+ (reset! disable* disable?)
+ nil)
- (mf/use-effect
- (fn []
- #(when (and (some? @node-ref) @rendered?)
+ (mf/with-effect []
+ (fn []
+ (when (and (some? @node-ref)
+ ^boolean @rendered?)
(mf/unmount @node-ref)
(reset! node-ref nil)
(reset! rendered? false)
- (when (some? @observer-ref)
- (.disconnect @observer-ref)
- (reset! observer-ref nil)))))
-
- ;; When the thumbnail-data is empty we regenerate the thumbnail
- (mf/use-effect
- (mf/deps (:selrect shape) thumbnail-data)
- (fn []
- (let [{:keys [width height]} (:selrect shape)]
- (p/then (wapi/empty-png-size width height)
- (fn [data]
- (when (<= (count thumbnail-data) (+ 100 (count data)))
- (rx/push! updates-str :update)))))))
+ (when (some? @observer*)
+ (.disconnect @observer*)
+ (reset! observer* nil)))))
[on-load-frame-dom
- @render-frame?
+ @render-frame*
(mf/html
- [:& frame/frame-container {:bounds shape-bb
- :shape (cond-> shape
- (some? thumbnail-data)
- (assoc :thumbnail thumbnail-data))}
-
- (when @show-frame-thumbnail
- [:> frame/frame-thumbnail-image
- {:key (dm/str (:id shape))
- :bounds shape-bb
- :shape (cond-> shape
- (some? thumbnail-data)
- (assoc :thumbnail thumbnail-data))}])
-
- [:foreignObject {:x x
- :y y
- :width width
- :height height
- :opacity (when disable-fills? 0)}
- [:canvas.thumbnail-canvas
- {:key (dm/str "thumbnail-canvas-" (:id shape))
- :ref frame-canvas-ref
- :data-object-id (dm/str page-id (:id shape))
- :width width
- :height height
- :style {;; Safari has a problem with the positioning of the canvas. All this is to fix Safari behavior
- ;; https://bugs.webkit.org/show_bug.cgi?id=23113
- :display (when (cf/check-browser? :safari) "none")
- :position "fixed"
- :transform-origin "top left"
- :transform (when (cf/check-browser? :safari) (dm/fmt "scale(%)" zoom))
- ;; DEBUG
- :filter (when (debug? :thumbnails) "invert(1)")}}]]
+ [:& frame/frame-container {:bounds shape-bb :shape shape}
;; Safari don't support filters so instead we add a rectangle around the thumbnail
- (when (and (cf/check-browser? :safari) (debug? :thumbnails))
+ (when (and (cf/check-browser? :safari)
+ ^boolean debug?)
[:rect {:x (+ x 2)
:y (+ y 2)
:width (- width 4)
@@ -294,13 +238,32 @@
:stroke "blue"
:stroke-width 2}])
- (when (some? @image-url)
- [:foreignObject {:x x
- :y y
- :width fixed-width
- :height fixed-height}
- [:img {:ref frame-image-ref
- :src @image-url
- :width fixed-width
- :height fixed-height
- :on-load on-image-load}]])])]))
+ ;; This is similar to how double-buffering works.
+ ;; In svg-uri* we keep the SVG image that is used to
+ ;; render the bitmap until the bitmap is ready
+ ;; to be rendered on screen. Then we remove the
+ ;; svg and keep the bitmap one.
+ ;; This is the "buffer" that keeps the bitmap image.
+ (when ^boolean @bitmap-uri*
+ [:image.thumbnail-bitmap
+ {:x x
+ :y y
+ :width width
+ :height height
+ :href @bitmap-uri*
+ :style {:filter (when ^boolean debug? "sepia(1)")}
+ :on-load on-bitmap-load}])
+
+ ;; This is the "buffer" that keeps the SVG image.
+ (when ^boolean @svg-uri*
+ [:image.thumbnail-canvas
+ {:x x
+ :y y
+ :key (dm/str "thumbnail-canvas-" id)
+ :data-object-id (dm/str page-id id)
+ :width width
+ :height height
+ :ref frame-image-ref
+ :href @svg-uri*
+ :style {:filter (when ^boolean debug? "sepia(0.5)")}
+ :on-load on-svg-load}])])]))
diff --git a/frontend/src/app/main/worker.cljs b/frontend/src/app/main/worker.cljs
index 8ff2b31b7..ee5f399f3 100644
--- a/frontend/src/app/main/worker.cljs
+++ b/frontend/src/app/main/worker.cljs
@@ -26,13 +26,19 @@
(reset! instance worker)))
(defn ask!
- [message]
- (when @instance (uw/ask! @instance message)))
+ ([message]
+ (when @instance (uw/ask! @instance message)))
+ ([message transfer]
+ (when @instance (uw/ask! @instance message transfer))))
(defn ask-buffered!
- [message]
- (when @instance (uw/ask-buffered! @instance message)))
+ ([message]
+ (when @instance (uw/ask-buffered! @instance message)))
+ ([message transfer]
+ (when @instance (uw/ask-buffered! @instance message transfer))))
(defn ask-many!
- [message]
- (when @instance (uw/ask-many! @instance message)))
+ ([message]
+ (when @instance (uw/ask-many! @instance message)))
+ ([message transfer]
+ (when @instance (uw/ask-many! @instance message transfer))))
diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs
index 45cdfe529..3acd630d3 100644
--- a/frontend/src/app/util/text_svg_position.cljs
+++ b/frontend/src/app/util/text_svg_position.cljs
@@ -57,7 +57,7 @@
(-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font))
(load-font font)))
- (p/catch #(.error js/console (dm/str "Cannot load font" font-id) %)))))
+ (p/catch #(.error js/console (dm/str "Cannot load font " font-id) %)))))
(defn- calc-text-node-positions
[shape-id]
diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs
index 49f46f0bc..adf487c70 100644
--- a/frontend/src/app/util/webapi.cljs
+++ b/frontend/src/app/util/webapi.cljs
@@ -51,8 +51,8 @@
(defn revoke-uri
[url]
- (assert (string? url) "invalid arguments")
- (js/URL.revokeObjectURL url))
+ (when ^boolean (str/starts-with? url "blob:")
+ (js/URL.revokeObjectURL url)))
(defn create-uri
"Create a url from blob."
diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs
index 442186bd5..2100b5850 100644
--- a/frontend/src/app/util/worker.cljs
+++ b/frontend/src/app/util/worker.cljs
@@ -8,6 +8,7 @@
"A lightweight layer on top of webworkers api."
(:require
[app.common.uuid :as uuid]
+ [app.util.object :as obj]
[app.worker.messages :as wm]
[beicon.core :as rx]))
@@ -25,11 +26,14 @@
(rx/take-while #(not (:completed %)) ob)
(rx/take 1 ob)))
- data (wm/encode message)
+ transfer (:transfer message)
+ data (cond-> (wm/encode (dissoc message :transfer))
+ (some? transfer)
+ (obj/set! "transfer" transfer))
instance (:instance worker)]
(if (some? instance)
- (do (.postMessage instance data)
+ (do (.postMessage instance data transfer)
(->> (:stream worker)
(rx/filter #(= (:reply-to %) sender-id))
(take-messages)
@@ -38,27 +42,36 @@
(rx/empty)))))
(defn ask!
- [worker message]
- (send-message!
- worker
- {:sender-id (uuid/next)
- :payload message}))
+ ([worker message]
+ (ask! worker message nil))
+ ([worker message transfer]
+ (send-message!
+ worker
+ {:sender-id (uuid/next)
+ :payload message
+ :transfer transfer})))
(defn ask-many!
- [worker message]
- (send-message!
- worker
- {:sender-id (uuid/next)
- :payload message}
- {:many? true}))
+ ([worker message]
+ (ask-many! worker message nil))
+ ([worker message transfer]
+ (send-message!
+ worker
+ {:sender-id (uuid/next)
+ :payload message
+ :transfer transfer}
+ {:many? true})))
(defn ask-buffered!
- [worker message]
- (send-message!
- worker
- {:sender-id (uuid/next)
- :payload message
- :buffer? true}))
+ ([worker message]
+ (ask-buffered! worker message nil))
+ ([worker message transfer]
+ (send-message!
+ worker
+ {:sender-id (uuid/next)
+ :payload message
+ :buffer? true
+ :transfer transfer})))
(defn init
"Return a initialized webworker instance."
diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs
index 8b7c10a26..4b0171cb6 100644
--- a/frontend/src/app/worker.cljs
+++ b/frontend/src/app/worker.cljs
@@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.schema :as sm]
+ [app.util.object :as obj]
[app.worker.export]
[app.worker.impl :as impl]
[app.worker.import]
@@ -38,7 +39,7 @@
(defn- handle-message
"Process the message and returns to the client"
- [{:keys [sender-id payload] :as message}]
+ [{:keys [sender-id payload transfer] :as message}]
(dm/assert! (message? message))
(letfn [(post [msg]
(let [msg (-> msg (assoc :reply-to sender-id) (wm/encode))]
@@ -63,7 +64,7 @@
:completed true})))]
(try
- (let [result (impl/handler payload)
+ (let [result (impl/handler payload transfer)
promise? (p/promise? result)
stream? (or (rx/observable? result) (rx/subject? result))]
@@ -145,7 +146,10 @@
[event]
(when (nil? (.-source event))
(let [message (.-data event)
- message (wm/decode message)]
+ transfer (obj/get message "transfer")
+ message (cond-> (wm/decode message)
+ (some? transfer)
+ (assoc :transfer transfer))]
(if (:buffer? message)
(rx/push! buffer message)
(handle-message message)))))
diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs
index 08f15742b..479f40ce5 100644
--- a/frontend/src/app/worker/export.cljs
+++ b/frontend/src/app/worker/export.cljs
@@ -474,9 +474,9 @@
(->> (rx/from files)
(rx/mapcat
(fn [file]
- (->> (rp/command! :export-binfile {:file-id (:id file)
- :include-libraries? (= export-type :all)
- :embed-assets? (= export-type :merge)})
+ (->> (rp/cmd! :export-binfile {:file-id (:id file)
+ :include-libraries? (= export-type :all)
+ :embed-assets? (= export-type :merge)})
(rx/map #(hash-map :type :finish
:file-id (:id file)
:filename (:name file)
diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs
index 29aa8f002..2e533d894 100644
--- a/frontend/src/app/worker/import.cljs
+++ b/frontend/src/app/worker/import.cljs
@@ -672,7 +672,7 @@
:response-type :blob
:method :get})
(rx/map :body)
- (rx/mapcat #(rp/command! :import-binfile {:file %
+ (rx/mapcat #(rp/cmd! :import-binfile {:file %
:project-id project-id}))
(rx/map
(fn [_]
diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs
index 0ab917984..e67004fd6 100644
--- a/frontend/src/app/worker/thumbnails.cljs
+++ b/frontend/src/app/worker/thumbnails.cljs
@@ -16,6 +16,7 @@
[app.worker.impl :as impl]
[beicon.core :as rx]
[debug :refer [debug?]]
+ [promesa.core :as p]
[rumext.v2 :as mf]))
(log/set-level! :trace)
@@ -110,8 +111,8 @@
(rx/catch body-too-large? (constantly (rx/of nil)))
(rx/map (constantly params)))))
-(defmethod impl/handler :thumbnails/generate
- [{:keys [file-id revn features] :as message}]
+(defmethod impl/handler :thumbnails/generate-for-file
+ [{:keys [file-id revn features] :as message} _]
(letfn [(on-result [{:keys [data props]}]
{:data data
:fonts (:fonts props)})
@@ -130,3 +131,18 @@
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit")))
(rx/catch not-found? on-cache-miss)
(rx/map on-result)))))
+
+(defmethod impl/handler :thumbnails/render-offscreen-canvas
+ [_ ibpm]
+ (let [canvas (js/OffscreenCanvas. (.-width ^js ibpm) (.-height ^js ibpm))
+ ctx (.getContext ^js canvas "bitmaprenderer")]
+
+ (.transferFromImageBitmap ^js ctx ibpm)
+
+ (->> (.convertToBlob ^js canvas #js {:type "image/png"})
+ (p/fmap (fn [blob]
+ (js/console.log "[worker]: generated thumbnail")
+ {:result (.createObjectURL js/URL blob)}))
+ (p/fnly (fn [_]
+ (.close ^js ibpm))))))
+