From 48834f96d33902d6530e707f636f542e53d4f81c Mon Sep 17 00:00:00 2001 From: Aitor Date: Fri, 12 May 2023 13:38:29 +0200 Subject: [PATCH] :recycle: Refactor thumbnail rendering on workspace --- .../src/app/rpc/commands/files_thumbnails.clj | 238 ++++++++---- .../rpc_file_thumbnails_test.clj | 16 +- frontend/src/app/main/data/dashboard.cljs | 10 +- frontend/src/app/main/data/exports.cljs | 10 +- frontend/src/app/main/data/users.cljs | 6 +- frontend/src/app/main/data/workspace.cljs | 26 +- .../src/app/main/data/workspace/media.cljs | 2 +- .../app/main/data/workspace/svg_upload.cljs | 8 +- .../app/main/data/workspace/thumbnails.cljs | 117 +++--- frontend/src/app/main/repo.cljs | 186 +++------- frontend/src/app/main/ui/auth/login.cljs | 4 +- frontend/src/app/main/ui/auth/register.cljs | 4 +- .../src/app/main/ui/auth/verify_token.cljs | 2 +- .../src/app/main/ui/dashboard/file_menu.cljs | 2 +- frontend/src/app/main/ui/dashboard/grid.cljs | 3 +- frontend/src/app/main/ui/routes.cljs | 4 +- .../src/app/main/ui/settings/feedback.cljs | 2 +- frontend/src/app/main/ui/shapes/frame.cljs | 51 +-- .../src/app/main/ui/workspace/header.cljs | 2 +- .../app/main/ui/workspace/shapes/frame.cljs | 111 +++--- .../shapes/frame/thumbnail_render.cljs | 343 ++++++++---------- frontend/src/app/main/worker.cljs | 18 +- frontend/src/app/util/text_svg_position.cljs | 2 +- frontend/src/app/util/webapi.cljs | 4 +- frontend/src/app/util/worker.cljs | 51 ++- frontend/src/app/worker.cljs | 10 +- frontend/src/app/worker/export.cljs | 6 +- frontend/src/app/worker/import.cljs | 2 +- frontend/src/app/worker/thumbnails.cljs | 20 +- 29 files changed, 644 insertions(+), 616 deletions(-) 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)))))) +