♻️ Refactor thumbnail rendering on workspace

This commit is contained in:
Aitor 2023-05-12 13:38:29 +02:00 committed by Alonso Torres
parent 1d69da1ca5
commit 48834f96d3
29 changed files with 644 additions and 616 deletions

View file

@ -48,8 +48,9 @@
(let [sql (str/concat (let [sql (str/concat
"select object_id, data, media_id " "select object_id, data, media_id "
" from file_object_thumbnail" " from file_object_thumbnail"
" where file_id=?")] " where file_id=?")
(->> (db/exec! conn [sql file-id]) res (db/exec! conn [sql file-id])]
(->> res
(d/index-by :object-id (fn [row] (d/index-by :object-id (fn [row]
(or (some-> row :media-id get-public-uri) (or (some-> row :media-id get-public-uri)
(:data row)))) (:data row))))
@ -57,14 +58,16 @@
([conn file-id object-ids] ([conn file-id object-ids]
(let [sql (str/concat (let [sql (str/concat
"select object_id, data " "select object_id, data, media_id "
" from file_object_thumbnail" " from file_object_thumbnail"
" where file_id=? and object_id = ANY(?)") " where file_id=? and object_id = ANY(?)")
ids (db/create-array conn "text" (seq object-ids))] ids (db/create-array conn "text" (seq object-ids))
(->> (db/exec! conn [sql file-id ids]) res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id (fn [row] (d/index-by :object-id
(fn [row]
(or (some-> row :media-id get-public-uri) (or (some-> row :media-id get-public-uri)
(:data row)))))))) (:data row)))
res))))
(sv/defmethod ::get-file-object-thumbnails (sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails." "Retrieve a file object thumbnails."
@ -248,33 +251,52 @@
;; --- MUTATION COMMAND: upsert-file-object-thumbnail ;; --- 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) "insert into file_object_thumbnail(file_id, object_id, data)
values (?, ?, ?) values (?, ?, ?)
on conflict(file_id, object_id) do on conflict(file_id, object_id) do
update set data = ?;") update set data = ?;")
(def sql:upsert-object-thumbnail-2 (defn upsert-file-object-thumbnail!
[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})))
(s/def ::data (s/nilable ::us/string))
(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]))
(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)
(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) "insert into file_object_thumbnail(file_id, object_id, media_id)
values (?, ?, ?) values (?, ?, ?)
on conflict(file_id, object_id) do on conflict(file_id, object_id) do
update set media_id = ?;") update set media_id = ?;")
(defn upsert-file-object-thumbnail! (defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} {:keys [file-id object-id] :as params}] [{:keys [::db/conn ::sto/storage]} file-id object-id media]
;; NOTE: params can come with data set but with `nil` value, so we (let [path (:path media)
;; need first check the existence of the key and then the value. mtype (:mtype media)
(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) hash (sto/calculate-hash path)
data (-> (sto/content path) data (-> (sto/content path)
(sto/wrap-with-hash hash)) (sto/wrap-with-hash hash))
@ -284,58 +306,117 @@
:content-type mtype :content-type mtype
:bucket "file-object-thumbnail"})] :bucket "file-object-thumbnail"})]
(db/exec-one! conn [sql:upsert-object-thumbnail-2 file-id object-id (:id media) (:id media)])) (db/exec-one! conn [sql:create-object-thumbnail file-id object-id
(db/delete! conn :file-object-thumbnail {:file-id file-id :object-id object-id})))) (:id media) (:id media)])))
;; FIXME: change it on validation refactor
(s/def ::data (s/nilable ::us/string))
(s/def ::media (s/nilable ::media/upload)) (s/def ::media (s/nilable ::media/upload))
(s/def ::object-id ::us/string) (s/def ::create-file-object-thumbnail
(s/def ::upsert-file-object-thumbnail
(s/keys :req [::rpc/profile-id] (s/keys :req [::rpc/profile-id]
:req-un [::file-id ::object-id] :req-un [::file-id ::object-id ::media]))
:opt-un [::data ::media]))
(sv/defmethod ::upsert-file-object-thumbnail (sv/defmethod ::create-file-object-thumbnail
{::doc/added "1.17" {:doc/added "1.19"
::audit/skip true} ::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{: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)
(assert (or (contains? params :data) (when-not (db/read-only? conn)
(contains? params :media))) (-> 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] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [cfg (-> cfg (-> cfg
(update ::sto/storage media/configure-assets-storage) (update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn))] (assoc ::db/conn conn)
(upsert-file-object-thumbnail! cfg params) (delete-file-object-thumbnail! file-id object-id))
nil)))) nil)))
;; --- MUTATION COMMAND: upsert-file-thumbnail ;; --- MUTATION COMMAND: upsert-file-thumbnail
(def ^:private sql:upsert-file-thumbnail (def ^:private sql:upsert-file-thumbnail
"insert into file_thumbnail (file_id, revn, data, media_id, props) "insert into file_thumbnail (file_id, revn, data, props)
values (?, ?, ?, ?, ?::jsonb) values (?, ?, ?, ?::jsonb)
on conflict(file_id, revn) do 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! (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 {}))] (let [props (db/tjson (or props {}))]
(cond
(contains? params :data)
(when-let [data (:data params)]
(db/exec-one! conn [sql:upsert-file-thumbnail (db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data nil props data nil props])) file-id revn data props data props])))
(contains? params :media)
(when-let [{:keys [path mtype] :as media} (:media params)] (s/def ::revn ::us/integer)
(let [_ (media/validate-media-type! media) (s/def ::props map?)
_ (media/validate-media-size! media)
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id]
: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)
(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) hash (sto/calculate-hash path)
data (-> (sto/content path) data (-> (sto/content path)
(sto/wrap-with-hash hash)) (sto/wrap-with-hash hash))
@ -344,29 +425,26 @@
::sto/deduplicate? false ::sto/deduplicate? false
:content-type mtype :content-type mtype
:bucket "file-thumbnail"})] :bucket "file-thumbnail"})]
(db/exec-one! conn [sql:upsert-file-thumbnail (db/exec-one! conn [sql:create-file-thumbnail file-id revn
file-id revn nil (:id media) props nil (:id media) props])))))) (:id media) props
(:id media) props])))
(s/def ::revn ::us/integer)
(s/def ::props map?)
(s/def ::media ::media/upload) (s/def ::media ::media/upload)
(s/def ::create-file-thumbnail
(s/def ::upsert-file-thumbnail
(s/keys :req [::rpc/profile-id] (s/keys :req [::rpc/profile-id]
:req-un [::file-id ::revn ::props] :req-un [::file-id ::revn ::props ::media]))
:opt-un [::data ::media]))
(sv/defmethod ::upsert-file-thumbnail (sv/defmethod ::create-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the "Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails." grid thumbnails."
{::doc/added "1.17" {::doc/added "1.19"
::audit/skip true} ::audit/skip true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [cfg (-> cfg (-> cfg
(update ::sto/storage media/configure-assets-storage) (update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn))] (assoc ::db/conn conn)
(upsert-file-thumbnail! cfg params)) (create-file-thumbnail! params))
nil))) nil)))

View file

@ -52,7 +52,7 @@
:parent-id uuid/zero :parent-id uuid/zero
:type :frame}}]) :type :frame}}])
data1 {::th/type :upsert-file-object-thumbnail data1 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file) :file-id (:id file)
:object-id "test-key-1" :object-id "test-key-1"
@ -61,7 +61,7 @@
:path (th/tempfile "backend_tests/test_files/sample.jpg") :path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"}} :mtype "image/jpeg"}}
data2 {::th/type :upsert-file-object-thumbnail data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file) :file-id (:id file)
:object-id (str page-id shid) :object-id (str page-id shid)
@ -156,7 +156,7 @@
:revn 1 :revn 1
:data "data:base64,1234123124"} :data "data:base64,1234123124"}
data2 {::th/type :upsert-file-thumbnail data2 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file) :file-id (:id file)
:props {} :props {}
@ -166,7 +166,7 @@
:path (th/tempfile "backend_tests/test_files/sample2.jpg") :path (th/tempfile "backend_tests/test_files/sample2.jpg")
:mtype "image/jpeg"}} :mtype "image/jpeg"}}
data3 {::th/type :upsert-file-thumbnail data3 {::th/type :create-file-thumbnail
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file) :file-id (:id file)
:props {} :props {}
@ -177,11 +177,11 @@
:mtype "image/jpeg"}}] :mtype "image/jpeg"}}]
(let [out (th/command! data1)] (let [out (th/command! data1)]
;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) (t/is (nil? (:result out))))
(let [out (th/command! data2)] (let [out (th/command! data2)]
;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result 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})] (let [row (th/db-get :storage-object {:id (:media-id row1)} {::db/remove-deleted? false})]
(t/is (nil? row))) (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 (t/deftest get-file-object-thumbnail
(let [storage (::sto/storage th/*system*) (let [storage (::sto/storage th/*system*)
@ -269,7 +269,7 @@
:object-id "test-key-1" :object-id "test-key-1"
:data "data:base64,1234123124"} :data "data:base64,1234123124"}
data2 {::th/type :upsert-file-object-thumbnail data2 {::th/type :create-file-object-thumbnail
::rpc/profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file) :file-id (:id file)
:object-id "test-key-2" :object-id "test-key-2"

View file

@ -137,7 +137,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [team-id (:current-team-id 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)))))) (rx/map team-webhooks-fetched))))))
;; --- EVENT: fetch-projects ;; --- EVENT: fetch-projects
@ -302,7 +302,7 @@
(ptk/reify ::fetch-builtin-templates (ptk/reify ::fetch-builtin-templates
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/command :retrieve-list-of-builtin-templates) (->> (rp/cmd! :retrieve-list-of-builtin-templates)
(rx/map builtin-templates-fetched))))) (rx/map builtin-templates-fetched)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -555,7 +555,7 @@
{:keys [on-success on-error] {:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/command! :delete-webhook params) (->> (rp/cmd! :delete-webhook params)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -578,7 +578,7 @@
{:keys [on-success on-error] {:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/command! :update-webhook params) (->> (rp/cmd! :update-webhook params)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -598,7 +598,7 @@
{:keys [on-success on-error] {:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/command! :create-webhook params) (->> (rp/cmd! :create-webhook params)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))

View file

@ -145,7 +145,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(when (= status "ended") (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/delay 500)
(rx/map #(dom/trigger-download filename %))))))) (rx/map #(dom/trigger-download filename %)))))))
@ -165,9 +165,9 @@
:wait true}] :wait true}]
(rx/concat (rx/concat
(rx/of ::dwp/force-persist) (rx/of ::dwp/force-persist)
(->> (rp/command! :export params) (->> (rp/cmd! :export params)
(rx/mapcat (fn [{:keys [id filename]}] (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] (rx/map (fn [data]
(dom/trigger-download filename data) (dom/trigger-download filename data)
(clear-export-state uuid/zero)))))) (clear-export-state uuid/zero))))))
@ -213,7 +213,7 @@
;; Launch the exportation process and stores the resource id ;; Launch the exportation process and stores the resource id
;; locally. ;; locally.
(->> (rp/command! :export params) (->> (rp/cmd! :export params)
(rx/map (fn [{:keys [id] :as resource}] (rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id) (vreset! resource-id id)
(initialize-export-status exports cmd resource)))) (initialize-export-status exports cmd resource))))

View file

@ -535,7 +535,7 @@
(ptk/reify ::fetch-access-tokens (ptk/reify ::fetch-access-tokens
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/command! :get-access-tokens) (->> (rp/cmd! :get-access-tokens)
(rx/map access-tokens-fetched))))) (rx/map access-tokens-fetched)))))
;; --- EVENT: create-access-token ;; --- EVENT: create-access-token
@ -555,7 +555,7 @@
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/command! :create-access-token params) (->> (rp/cmd! :create-access-token params)
(rx/map access-token-created) (rx/map access-token-created)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -571,6 +571,6 @@
(let [{:keys [on-success on-error] (let [{:keys [on-success on-error]
:or {on-success identity :or {on-success identity
on-error rx/throw}} (meta params)] on-error rx/throw}} (meta params)]
(->> (rp/command! :delete-access-token params) (->> (rp/cmd! :delete-access-token params)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))

View file

@ -267,6 +267,29 @@
(when needs-update? (when needs-update?
(rx/of (dwl/notify-sync-file file-id))))))) (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 (defn- fetch-bundle
[project-id file-id] [project-id file-id]
(ptk/reify ::fetch-bundle (ptk/reify ::fetch-bundle
@ -285,9 +308,8 @@
;; WTF is this? ;; WTF is this?
share-id (-> state :viewer-local :share-id) share-id (-> state :viewer-local :share-id)
stoper (rx/filter (ptk/type? ::fetch-bundle) stream)] stoper (rx/filter (ptk/type? ::fetch-bundle) stream)]
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) (->> (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-project {:id project-id})
(rp/cmd! :get-team-users {:file-id file-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})) (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id}))

View file

@ -94,7 +94,7 @@
(->> (rx/from uris) (->> (rx/from uris)
(rx/filter (comp not svg-url?)) (rx/filter (comp not svg-url?))
(rx/map prepare) (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/do on-image))
(->> (rx/from uris) (->> (rx/from uris)

View file

@ -478,7 +478,7 @@
:content (data-uri->blob uri)} :content (data-uri->blob uri)}
{:name (extract-name uri)})))) {:name (extract-name uri)}))))
(rx/mapcat (fn [uri-data] (rx/mapcat (fn [uri-data]
(->> (rp/command! (if (contains? uri-data :content) (->> (rp/cmd! (if (contains? uri-data :content)
:upload-file-media-object :upload-file-media-object
:create-file-media-object-from-url) :create-file-media-object-from-url)
uri-data) uri-data)

View file

@ -14,8 +14,10 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.worker :as uw]
[app.util.dom :as dom] [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] [app.util.webapi :as wapi]
[beicon.core :as rx] [beicon.core :as rx]
[potok.core :as ptk])) [potok.core :as ptk]))
@ -29,28 +31,22 @@
(rx/filter #(= % id)) (rx/filter #(= % id))
(rx/take 1))) (rx/take 1)))
(defn thumbnail-canvas-blob-stream (defn get-thumbnail
[object-id] [object-id]
;; Look for the thumbnail canvas to send the data to the backend ;; 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 stopper (->> st/stream
(rx/filter (ptk/type? :app.main.data.workspace/finalize-page)) (rx/filter (ptk/type? :app.main.data.workspace/finalize-page))
(rx/take 1))] (rx/take 1))]
;; renders #svg image
(if (some? node) (if (some? node)
;; Success: we generate the blob (async call) (->> (rx/from (js/createImageBitmap node))
(rx/create (rx/switch-map #(uw/ask! {:cmd :thumbnails/render-offscreen-canvas} %))
(fn [subs] (rx/map :result))
(ts/raf
(fn []
(.toBlob node (fn [blob]
(rx/push! subs blob)
#_(rx/end! subs))
"image/png")))
(constantly nil)))
;; Not found, we retry after delay ;; Not found, we retry after delay
(->> (rx/timer 250) (->> (rx/timer 250)
(rx/flat-map #(thumbnail-canvas-blob-stream object-id)) (rx/merge-map (partial get-thumbnail object-id))
(rx/take-until stopper))))) (rx/take-until stopper)))))
(defn clear-thumbnail (defn clear-thumbnail
@ -59,8 +55,33 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [object-id (dm/str page-id frame-id)] (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))))) (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 (defn update-thumbnail
"Updates the thumbnail information for the given frame `id`" "Updates the thumbnail information for the given frame `id`"
([page-id frame-id] ([page-id frame-id]
@ -71,38 +92,32 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [object-id (dm/str page-id frame-id) (let [object-id (dm/str page-id frame-id)
file-id (or file-id (:current-file-id state)) 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}]
(rx/concat (rx/concat
;; Delete the thumbnail first so if we interrupt we can regenerate after ;; Delete the thumbnail first so if we interrupt we can regenerate after
(->> (rp/cmd! :upsert-file-object-thumbnail params) (->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id})
(rx/catch #(rx/empty))) (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))
;; Send the update to the back-end ;; 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 (rx/merge-map
(fn [blob] (fn [uri]
(if (some? blob)
(wapi/read-file-as-data-url blob)
(rx/of nil))))
(rx/merge-map
(fn [data]
(if (and (some? data) (some? file-id))
(let [params (assoc params :data data)]
(rx/merge (rx/merge
;; Update the local copy of the thumbnails so we don't need to request it again (rx/of (set-workspace-thumbnail object-id uri))
(rx/of #(update % :workspace-thumbnails assoc object-id data))
(->> (rp/cmd! :upsert-file-object-thumbnail params) (->> (http/send! {:uri uri :response-type :blob :method :get})
(rx/catch #(rx/empty)) (rx/map :body)
(rx/ignore)))) (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/catch #(do (.error js/console %)
(rx/empty)))))))))) (rx/empty))))))))))
@ -137,6 +152,7 @@
(and new-frame-id (not= uuid/zero new-frame-id)) (and new-frame-id (not= uuid/zero new-frame-id))
(conj [page-id new-frame-id]))))] (conj [page-id new-frame-id]))))]
(into #{} (into #{}
(comp (mapcat extract-ids) (comp (mapcat extract-ids)
(mapcat get-frame-id)) (mapcat get-frame-id))
@ -154,7 +170,7 @@
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
(= ::watch-state-changes (ptk/type %))))) (= ::watch-state-changes (ptk/type %)))))
workspace-data-str workspace-data-s
(->> (rx/concat (->> (rx/concat
(rx/of nil) (rx/of nil)
(rx/from-atom refs/workspace-data {:emit-current-value? true})) (rx/from-atom refs/workspace-data {:emit-current-value? true}))
@ -162,33 +178,24 @@
;; deleted objects ;; deleted objects
(rx/buffer 2 1)) (rx/buffer 2 1))
change-str change-s
(->> stream (->> stream
(rx/filter #(or (dch/commit-changes? %) (rx/filter #(or (dch/commit-changes? %)
(= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change)))
(rx/observe-on :async)) (rx/observe-on :async))
frame-changes-str frame-changes-s
(->> change-str (->> change-s
(rx/with-latest-from workspace-data-str) (rx/with-latest-from workspace-data-s)
(rx/flat-map extract-frame-changes) (rx/flat-map extract-frame-changes)
(rx/share))] (rx/share))]
(->> (rx/merge (->> (rx/merge
(->> frame-changes-str (->> frame-changes-s
(rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state)))) (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)))) (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/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state))))
(rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id))))) (rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id)))))
(rx/take-until stopper)))))) (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)))))

View file

@ -10,29 +10,8 @@
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.util.http :as http] [app.util.http :as http]
[beicon.core :as rx])) [beicon.core :as rx]
[cuerdas.core :as str]))
(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)
(defn handle-response (defn handle-response
[{:keys [status body] :as response}] [{:keys [status body] :as response}]
@ -65,45 +44,41 @@
:status status :status status
:data body}))) :data body})))
(defn- send-query! (def default-options
"A simple helper for send and receive transit data on the penpot {:update-file {:query-params [:id]}
query api." :get-raw-file {:rename-to :get-file :raw-transit? true}
([id params] :upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
(send-query! id params nil)) :create-file-object-thumbnail {:query-params [:file-id :object-id]
([id params {:keys [raw-transit?]}] :form-data? true}
(let [decode-transit (if raw-transit? :export-binfile {:response-type :blob}
http/conditional-error-decode-transit :import-binfile {:form-data? true}
http/conditional-decode-transit)] :retrieve-list-of-builtin-templates {:query-params :all}
(->> (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)))))
(defn- send-mutation! (defn- send!
"A simple helper for a common case of sending and receiving transit "A simple helper for a common case of sending and receiving transit
data to the penpot mutation api." data to the penpot mutation api."
[id params] [id params options]
(->> (http/send! {:method :post (let [{:keys [response-type
:uri (u/join @cf/public-uri "api/rpc/mutation/" (name id)) form-data?
:headers {"accept" "application/transit+json"} raw-transit?
:credentials "include" query-params
:body (http/transit-data params)}) rename-to]}
(rx/map http/conditional-decode-transit) (-> (get default-options id)
(rx/mapcat handle-response))) (merge options))
(defn- send-command! decode-fn (if raw-transit?
"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?
http/conditional-error-decode-transit http/conditional-error-decode-transit
http/conditional-decode-transit) http/conditional-decode-transit)
method (if (isa? id ::query) :get :post)]
(->> (http/send! {:method method 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)) :uri (u/join @cf/public-uri "api/rpc/command/" (name id))
:credentials "include" :credentials "include"
:headers {"accept" "application/transit+json"} :headers {"accept" "application/transit+json"}
@ -113,72 +88,22 @@
(http/transit-data params))) (http/transit-data params)))
:query (if (= method :get) :query (if (= method :get)
params params
(if forward-query-params (if query-params
(select-keys params forward-query-params) (select-keys params query-params)
nil)) nil))
:response-type (or response-type :text)}) :response-type (or response-type :text)}]
(->> (http/send! request)
(rx/map decode-fn) (rx/map decode-fn)
(rx/mapcat handle-response)))) (rx/mapcat handle-response))))
(defn- dispatch [& args] (first args)) (defmulti cmd! (fn [id _] id))
(defmulti query dispatch) (defmethod cmd! :default
(defmulti mutation dispatch)
(defmulti command dispatch)
(defmethod query :default
[id params] [id params]
(send-query! id params)) (send! id params nil))
(defmethod command :get-raw-file (defmethod cmd! :login-with-oidc
[_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
[_ {:keys [provider] :as params}] [_ {:keys [provider] :as params}]
(let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider)) (let [uri (u/join @cf/public-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)] params (dissoc params :provider)]
@ -199,7 +124,7 @@
(rx/map http/conditional-decode-transit) (rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))) (rx/mapcat handle-response)))
(defmethod command :export (defmethod cmd! :export
[_ params] [_ params]
(let [default {:wait false :blob? false}] (let [default {:wait false :blob? false}]
(send-export (merge default params)))) (send-export (merge default params))))
@ -208,16 +133,7 @@
(derive :update-profile-photo ::multipart-upload) (derive :update-profile-photo ::multipart-upload)
(derive :update-team-photo ::multipart-upload) (derive :update-team-photo ::multipart-upload)
(defmethod mutation ::multipart-upload (defmethod cmd! ::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
[id params] [id params]
(->> (http/send! {:method :post (->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/rpc/command/" (name id)) :uri (u/join @cf/public-uri "api/rpc/command/" (name id))

View file

@ -37,7 +37,7 @@
(defn- login-with-oidc (defn- login-with-oidc
[event provider params] [event provider params]
(dom/prevent-default event) (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}] (rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(if redirect-uri (if redirect-uri
(.replace js/location redirect-uri) (.replace js/location redirect-uri)
@ -57,7 +57,7 @@
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)] (let [{:keys [on-error]} (meta params)]
(->> (rp/command! :login-with-ldap params) (->> (rp/cmd! :login-with-ldap params)
(rx/subs (fn [profile] (rx/subs (fn [profile]
(if-let [token (:invitation-token profile)] (if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})) (st/emit! (rt/nav :auth-verify-token {} {:token token}))

View file

@ -101,7 +101,7 @@
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [cdata (:clean-data @form)] (let [cdata (:clean-data @form)]
(->> (rp/command! :prepare-register-profile cdata) (->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params)) (rx/map #(merge % params))
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs (rx/subs
@ -232,7 +232,7 @@
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [params (:clean-data @form)] (let [params (:clean-data @form)]
(->> (rp/command! :register-profile params) (->> (rp/cmd! :register-profile params)
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs on-success (rx/subs on-success
(partial handle-register-error form))))))] (partial handle-register-error form))))))]

View file

@ -65,7 +65,7 @@
(mf/with-effect [] (mf/with-effect []
(dom/set-html-title (tr "title.default")) (dom/set-html-title (tr "title.default"))
(->> (rp/command! :verify-token {:token token}) (->> (rp/cmd! :verify-token {:token token})
(rx/subs (rx/subs
(fn [tdata] (fn [tdata]
(handle-token tdata)) (handle-token tdata))

View file

@ -177,7 +177,7 @@
(->> (rx/from files) (->> (rx/from files)
(rx/flat-map (rx/flat-map
(fn [file] (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/map #(assoc file :has-libraries? %)))))
(rx/reduce conj []) (rx/reduce conj [])
(rx/subs (rx/subs

View file

@ -46,7 +46,8 @@
(let [features (cond-> ffeat/enabled (let [features (cond-> ffeat/enabled
(features/active-feature? :components-v2) (features/active-feature? :components-v2)
(conj "components/v2"))] (conj "components/v2"))]
(wrk/ask! {:cmd :thumbnails/generate
(wrk/ask! {:cmd :thumbnails/generate-for-file
:revn (:revn file) :revn (:revn file)
:file-id (:id file) :file-id (:id file)
:file-name (:name file) :file-name (:name file)

View file

@ -98,7 +98,7 @@
;; We just recheck with an additional profile request; this avoids ;; We just recheck with an additional profile request; this avoids
;; some race conditions that causes unexpected redirects on ;; some race conditions that causes unexpected redirects on
;; invitations workflows (and probably other cases). ;; invitations workflows (and probably other cases).
(->> (rp/command! :get-profile) (->> (rp/cmd! :get-profile)
(rx/subs (fn [{:keys [id] :as profile}] (rx/subs (fn [{:keys [id] :as profile}]
(if (= id uuid/zero) (if (= id uuid/zero)
(st/emit! (rt/nav :auth-login)) (st/emit! (rt/nav :auth-login))

View file

@ -55,7 +55,7 @@
(fn [form _] (fn [form _]
(reset! loading true) (reset! loading true)
(let [data (:clean-data @form)] (let [data (:clean-data @form)]
(->> (rp/command! :send-user-feedback data) (->> (rp/cmd! :send-user-feedback data)
(rx/subs on-succes on-error)))))] (rx/subs on-succes on-error)))))]
[:& fm/form {:class "feedback-form" [:& fm/form {:class "feedback-form"

View file

@ -88,36 +88,41 @@
(mf/defc frame-thumbnail-image (mf/defc frame-thumbnail-image
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [shape (unchecked-get props "shape")
bounds (or (unchecked-get props "bounds")
(gsh/points->selrect (:points shape)))
(let [shape (obj/get props "shape") shape-id (:id shape)
bounds (or (obj/get props "bounds") (gsh/points->selrect (:points shape)))] thumb (:thumbnail shape)
debug? (debug? :thumbnails)
safari? (cf/check-browser? :safari)]
(when (:thumbnail shape)
[:* [:*
[:image.frame-thumbnail [:image.frame-thumbnail
{:id (dm/str "thumbnail-" (:id shape)) {:id (dm/str "thumbnail-" shape-id)
:href (:thumbnail shape) :href thumb
:decoding "async"
:x (:x bounds) :x (:x bounds)
:y (:y bounds) :y (:y bounds)
:width (:width bounds) :width (:width bounds)
:height (:height bounds) :height (:height bounds)
;; DEBUG :style {:filter (when (and (not ^boolean safari?) ^boolean debug?) "sepia(1)")}}]
:style {:filter (when (and (not (cf/check-browser? :safari))(debug? :thumbnails)) "sepia(1)")}}]
;; Safari don't support filters so instead we add a rectangle around the thumbnail ;; Safari don't support filters so instead we add a rectangle around the thumbnail
(when (and (cf/check-browser? :safari) (debug? :thumbnails)) (when (and ^boolean safari? ^boolean debug?)
[:rect {:x (+ (:x bounds) 4) [:rect {:x (+ (:x bounds) 4)
:y (+ (:y bounds) 4) :y (+ (:y bounds) 4)
:width (- (:width bounds) 8) :width (- (:width bounds) 8)
:height (- (:height bounds) 8) :height (- (:height bounds) 8)
:stroke "red" :stroke "red"
:stroke-width 2}])]))) :stroke-width 2}])]))
(mf/defc frame-thumbnail (mf/defc frame-thumbnail
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [shape (obj/get props "shape")] (let [shape (unchecked-get props "shape")]
(when (:thumbnail shape) (when ^boolean (:thumbnail shape)
[:> frame-container props [:> frame-container props
[:> frame-thumbnail-image props]]))) [:> frame-thumbnail-image props]])))

View file

@ -179,7 +179,7 @@
(->> (rx/of file) (->> (rx/of file)
(rx/flat-map (rx/flat-map
(fn [file] (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/map #(assoc file :has-libraries? %)))))
(rx/reduce conj []) (rx/reduce conj [])
(rx/subs (rx/subs

View file

@ -11,7 +11,6 @@
[app.common.pages.helpers :as cph] [app.common.pages.helpers :as cph]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
@ -80,71 +79,69 @@
[props] [props]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
frame-id (:id shape) thumbnail? (unchecked-get props "thumbnail?")
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)]
(fdm/use-dynamic-modifiers objects @node-ref modifiers)
(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))
force-render (mf/use-state false)
;; Thumbnail data
page-id (mf/use-ctx ctx/current-page-id) page-id (mf/use-ctx ctx/current-page-id)
frame-id (:id shape)
objects (wsh/lookup-page-objects @st/state)
node* (mf/use-var nil)
force-render* (mf/use-state false)
force-render? (deref force-render*)
;; when `true` we've called the mount for the frame ;; when `true` we've called the mount for the frame
rendered? (mf/use-var false) 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)
fonts (mf/with-memo [shape objects]
(ff/shape->fonts shape objects))
fonts (hooks/use-equal-memo fonts)
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers])) disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers]))
[on-load-frame-dom render-frame? thumbnail-renderer] [on-load-frame-dom render-frame? thumbnail-renderer]
(ftr/use-render-thumbnail page-id shape node-ref rendered? disable-thumbnail? @force-render) (ftr/use-render-thumbnail page-id shape node* rendered* disable-thumbnail? force-render?)
on-frame-load on-frame-load
(fns/use-node-store thumbnail? node-ref rendered? render-frame?)] (fns/use-node-store thumbnail? node* rendered* render-frame?)]
(mf/use-effect (fdm/use-dynamic-modifiers objects @node* modifiers)
(mf/deps fonts)
(fn []
(->> (rx/from fonts)
(rx/merge-map fonts/fetch-font-css)
(rx/ignore))))
(mf/use-effect (mf/with-effect []
(fn []
;; When a change in the data is received a "force-render" event is emitted ;; When a change in the data is received a "force-render" event is emitted
;; that will force the component to be mounted in memory ;; that will force the component to be mounted in memory
(let [sub (let [sub (->> (dwt/force-render-stream frame-id)
(->> (dwt/force-render-stream (:id shape)) (rx/take-while #(not @rendered*))
(rx/take-while #(not @rendered?)) (rx/subs #(reset! force-render* true)))]
(rx/subs #(reset! force-render true)))] #(some-> sub rx/dispose!)))
#(when sub
(rx/dispose! sub)))))
(mf/use-effect (mf/with-effect [shape fonts thumbnail? on-load-frame-dom force-render? render-frame?]
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render render-frame?) (when (and (some? @node*)
(fn [] (or @rendered*
(when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render render-frame?)) (not thumbnail?)
(mf/mount force-render?
(mf/element frame-shape render-frame?))
#js {:ref on-load-frame-dom :shape shape :fonts fonts}) (let [elem (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts})]
(mf/mount elem @node*)
@node-ref) (when (not @rendered*)
(when (not @rendered?) (reset! rendered? true))))) (reset! rendered* true)))))
[:& shape-container {:shape shape} [:& shape-container {:shape shape}
[:g.frame-container {:id (dm/str "frame-container-" (:id shape)) [:g.frame-container {:id (dm/str "frame-container-" frame-id)
:key "frame-container" :key "frame-container"
:ref on-frame-load :ref on-frame-load
:opacity (when (:hidden shape) 0)} :opacity (when (:hidden shape) 0)}
[:& ff/fontfaces-style {:fonts fonts}] [:& ff/fontfaces-style {:fonts fonts}]
[:g.frame-thumbnail-wrapper [:g.frame-thumbnail-wrapper
{:id (dm/str "thumbnail-container-" (:id shape)) {:id (dm/str "thumbnail-container-" frame-id)
;; Hide the thumbnail when not displaying ;; Hide the thumbnail when not displaying
:opacity (when-not thumbnail? 0)} :opacity (when-not thumbnail? 0)}
thumbnail-renderer]]]))))) thumbnail-renderer]]
]))))

View file

@ -22,27 +22,8 @@
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[debug :refer [debug?]] [debug :refer [debug?]]
[promesa.core :as p]
[rumext.v2 :as mf])) [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 (defn- remove-image-loading
"Remove the changes related to change a url for its embed value. This is necessary "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." so we don't have to recalculate the thumbnail when the image loads."
@ -57,18 +38,38 @@
(str/starts-with? (.-oldValue change) "http")))))) (str/starts-with? (.-oldValue change) "http"))))))
[value])) [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 "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"%\" width=\"%\" height=\"%\" fill=\"none\">% %</svg>"
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 (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] [page-id {:keys [id] :as shape} node-ref rendered? disable? force-render]
(let [frame-canvas-ref (mf/use-ref nil) (let [frame-image-ref (mf/use-ref nil)
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/with-memo [id]
(refs/all-children-objects id))
all-children-ref (mf/use-memo (mf/deps id) #(refs/all-children-objects id))
all-children (mf/deref all-children-ref) all-children (mf/deref all-children-ref)
{:keys [x y width height] :as shape-bb} {:keys [x y width height] :as shape-bb}
@ -83,51 +84,46 @@
[(/ (* width (mth/clamp height 250 2000)) height) [(/ (* width (mth/clamp height 250 2000)) height)
(mth/clamp height 250 2000)]) (mth/clamp height 250 2000)])
image-url (mf/use-state nil) svg-uri* (mf/use-state nil)
observer-ref (mf/use-var 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-uri-ref (mf/with-memo [page-id id]
(refs/thumbnail-frame-data page-id id))
thumbnail-data-ref (mf/use-memo (mf/deps page-id id) #(refs/thumbnail-frame-data page-id id)) thumbnail-uri (mf/deref thumbnail-uri-ref)
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)
;; State to indicate to the parent that should render the frame ;; 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-bitmap-load
(mf/use-fn
on-image-load
(mf/use-callback
(mf/deps @show-frame-thumbnail)
(fn [] (fn []
(let [canvas-node (mf/ref-val frame-canvas-ref) ;; We revoke the SVG Blob URI to free memory only when we
img-node (mf/ref-val frame-image-ref)] ;; are sure that it is not used anymore.
(when (draw-thumbnail-canvas! canvas-node img-node) (wapi/revoke-uri @svg-uri*)
(when-not (cf/check-browser? :safari) (reset! svg-uri* nil)))
(reset! image-url nil))
on-svg-load
(mf/use-callback
(fn []
(let [image-node (mf/ref-val frame-image-ref)]
(dom/set-data! image-node "ready" "true")
(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 ;; If we don't have the thumbnail data saved (normally the first load) we update the data
;; when available ;; when available
(when (not @thumbnail-data-ref) (when (not ^boolean @thumbnail-uri-ref)
(st/emit! (dwt/update-thumbnail page-id id) )) (st/emit! (dwt/update-thumbnail page-id id)))
(reset! render-frame? false))))) (reset! render-frame* false))))
generate-thumbnail generate-thumbnail
(mf/use-callback (mf/use-fn
(mf/deps id)
(fn generate-thumbnail [] (fn generate-thumbnail []
(try (try
;; When starting generating the canvas we mark it as not ready so its not send to back until ;; 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] (let [node @node-ref]
(if (dom/has-children? node) (if (dom/has-children? node)
;; The frame-content need to have children in order to generate the thumbnail ;; 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")) (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)]
{:keys [x y width height]} @shape-bb-ref (reset! svg-uri* url))
viewbox (dm/str x " " y " " width " " height)
;; This is way faster than creating a node through the DOM API
svg-data
(dm/fmt "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"%\" width=\"%\" height=\"%\" fill=\"none\">% %</svg>"
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))
;; Node not yet ready, we schedule a new generation ;; Node not yet ready, we schedule a new generation
(ts/schedule generate-thumbnail))) (ts/raf generate-thumbnail)))
(catch :default e (catch :default e
(.error js/console e))))) (.error js/console e)))))
on-change-frame on-change-frame
(mf/use-callback (mf/use-fn
(mf/deps id)
(fn [] (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']")) (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']")))] loading-fonts? (some? (dom/query (dm/str "#frame-container-" id " > style[data-loading='true']")))]
(when (and (not loading-images?) (not loading-fonts?)) (when (and (not loading-images?)
(not loading-fonts?))
(generate-thumbnail) (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 on-update-frame
(mf/use-callback (mf/use-fn
(fn [] (fn []
(let [canvas-node (mf/ref-val frame-canvas-ref)] (let [image-node (mf/ref-val frame-image-ref)]
(when (not= "false" (dom/get-data canvas-node "ready")) (when (not= "false" (dom/get-data image-node "ready"))
(dom/set-data! canvas-node "ready" "false"))) (dom/set-data! image-node "ready" "false")))
(when (not @disable-ref?) (when-not ^boolean @disable*
(reset! render-frame? true) (reset! render-frame* true)
(reset! regenerate-thumbnail true)))) (reset! regenerate* true))))
on-load-frame-dom on-load-frame-dom
(mf/use-callback (mf/use-fn
(fn [node] (fn [node]
(when (and (some? node) (nil? @observer-ref)) (when (and (some? node)
(when-not (some? @thumbnail-data-ref) (nil? @observer*))
(rx/push! updates-str :update)) (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}) (.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/with-effect [thumbnail-uri]
(mf/deps thumbnail-data) (when (some? thumbnail-uri)
(fn [] (reset! bitmap-uri* thumbnail-uri)))
(when (and (some? prev-thumbnail-data) (nil? thumbnail-data))
(rx/push! updates-str :update))))
(mf/use-effect (mf/with-effect [force-render]
(mf/deps force-render) (when ^boolean force-render
(fn [] (rx/push! updates-s :update)))
(when force-render
(rx/push! updates-str :update))))
(mf/use-effect (mf/with-effect []
(fn [] (let [subid (->> updates-s
(let [subid (->> updates-str
(rx/map remove-image-loading) (rx/map remove-image-loading)
(rx/filter d/not-empty?) (rx/filter d/not-empty?)
(rx/catch (fn [err] (.error js/console err))) (rx/catch (fn [err] (.error js/console err)))
(rx/subs on-update-frame))] (rx/subs on-update-frame))]
#(rx/dispose! subid)))) (partial rx/dispose! subid)))
;; on-change-frame will get every change in the frame ;; on-change-frame will get every change in the frame
(mf/use-effect (mf/with-effect []
(fn [] (let [subid (->> updates-s
(let [subid (->> updates-str
(rx/debounce 400) (rx/debounce 400)
(rx/observe-on :af) (rx/observe-on :af)
(rx/catch (fn [err] (.error js/console err))) (rx/catch (fn [err] (.error js/console err)))
(rx/subs on-change-frame))] (rx/subs on-change-frame))]
#(rx/dispose! subid)))) (partial rx/dispose! subid)))
(mf/use-effect (mf/with-effect [disable?]
(mf/deps disable?) (when (and ^boolean disable?
(fn [] (not @disable*))
(when (and disable? (not @disable-ref?)) (rx/push! updates-s :update))
(rx/push! updates-str :update)) (reset! disable* disable?)
(reset! disable-ref? disable?))) nil)
(mf/use-effect (mf/with-effect []
(fn [] (fn []
#(when (and (some? @node-ref) @rendered?) (when (and (some? @node-ref)
^boolean @rendered?)
(mf/unmount @node-ref) (mf/unmount @node-ref)
(reset! node-ref nil) (reset! node-ref nil)
(reset! rendered? false) (reset! rendered? false)
(when (some? @observer-ref) (when (some? @observer*)
(.disconnect @observer-ref) (.disconnect @observer*)
(reset! observer-ref nil))))) (reset! observer* 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)))))))
[on-load-frame-dom [on-load-frame-dom
@render-frame? @render-frame*
(mf/html (mf/html
[:& frame/frame-container {:bounds shape-bb [:& frame/frame-container {:bounds shape-bb :shape shape}
: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)")}}]]
;; Safari don't support filters so instead we add a rectangle around the thumbnail ;; 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) [:rect {:x (+ x 2)
:y (+ y 2) :y (+ y 2)
:width (- width 4) :width (- width 4)
@ -294,13 +238,32 @@
:stroke "blue" :stroke "blue"
:stroke-width 2}]) :stroke-width 2}])
(when (some? @image-url) ;; This is similar to how double-buffering works.
[:foreignObject {:x x ;; 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 :y y
:width fixed-width :width width
:height fixed-height} :height height
[:img {:ref frame-image-ref :href @bitmap-uri*
:src @image-url :style {:filter (when ^boolean debug? "sepia(1)")}
:width fixed-width :on-load on-bitmap-load}])
:height fixed-height
:on-load on-image-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}])])]))

View file

@ -26,13 +26,19 @@
(reset! instance worker))) (reset! instance worker)))
(defn ask! (defn ask!
[message] ([message]
(when @instance (uw/ask! @instance message))) (when @instance (uw/ask! @instance message)))
([message transfer]
(when @instance (uw/ask! @instance message transfer))))
(defn ask-buffered! (defn ask-buffered!
[message] ([message]
(when @instance (uw/ask-buffered! @instance message))) (when @instance (uw/ask-buffered! @instance message)))
([message transfer]
(when @instance (uw/ask-buffered! @instance message transfer))))
(defn ask-many! (defn ask-many!
[message] ([message]
(when @instance (uw/ask-many! @instance message))) (when @instance (uw/ask-many! @instance message)))
([message transfer]
(when @instance (uw/ask-many! @instance message transfer))))

View file

@ -57,7 +57,7 @@
(-> (fonts/ensure-loaded! font-id) (-> (fonts/ensure-loaded! font-id)
(p/then #(when (not (dom/check-font? font)) (p/then #(when (not (dom/check-font? font))
(load-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 (defn- calc-text-node-positions
[shape-id] [shape-id]

View file

@ -51,8 +51,8 @@
(defn revoke-uri (defn revoke-uri
[url] [url]
(assert (string? url) "invalid arguments") (when ^boolean (str/starts-with? url "blob:")
(js/URL.revokeObjectURL url)) (js/URL.revokeObjectURL url)))
(defn create-uri (defn create-uri
"Create a url from blob." "Create a url from blob."

View file

@ -8,6 +8,7 @@
"A lightweight layer on top of webworkers api." "A lightweight layer on top of webworkers api."
(:require (:require
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.object :as obj]
[app.worker.messages :as wm] [app.worker.messages :as wm]
[beicon.core :as rx])) [beicon.core :as rx]))
@ -25,11 +26,14 @@
(rx/take-while #(not (:completed %)) ob) (rx/take-while #(not (:completed %)) ob)
(rx/take 1 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)] instance (:instance worker)]
(if (some? instance) (if (some? instance)
(do (.postMessage instance data) (do (.postMessage instance data transfer)
(->> (:stream worker) (->> (:stream worker)
(rx/filter #(= (:reply-to %) sender-id)) (rx/filter #(= (:reply-to %) sender-id))
(take-messages) (take-messages)
@ -38,27 +42,36 @@
(rx/empty))))) (rx/empty)))))
(defn ask! (defn ask!
[worker message] ([worker message]
(send-message! (ask! worker message nil))
worker ([worker message transfer]
{:sender-id (uuid/next)
:payload message}))
(defn ask-many!
[worker message]
(send-message!
worker
{:sender-id (uuid/next)
:payload message}
{:many? true}))
(defn ask-buffered!
[worker message]
(send-message! (send-message!
worker worker
{:sender-id (uuid/next) {:sender-id (uuid/next)
:payload message :payload message
:buffer? true})) :transfer transfer})))
(defn ask-many!
([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]
(ask-buffered! worker message nil))
([worker message transfer]
(send-message!
worker
{:sender-id (uuid/next)
:payload message
:buffer? true
:transfer transfer})))
(defn init (defn init
"Return a initialized webworker instance." "Return a initialized webworker instance."

View file

@ -9,6 +9,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.util.object :as obj]
[app.worker.export] [app.worker.export]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[app.worker.import] [app.worker.import]
@ -38,7 +39,7 @@
(defn- handle-message (defn- handle-message
"Process the message and returns to the client" "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)) (dm/assert! (message? message))
(letfn [(post [msg] (letfn [(post [msg]
(let [msg (-> msg (assoc :reply-to sender-id) (wm/encode))] (let [msg (-> msg (assoc :reply-to sender-id) (wm/encode))]
@ -63,7 +64,7 @@
:completed true})))] :completed true})))]
(try (try
(let [result (impl/handler payload) (let [result (impl/handler payload transfer)
promise? (p/promise? result) promise? (p/promise? result)
stream? (or (rx/observable? result) (rx/subject? result))] stream? (or (rx/observable? result) (rx/subject? result))]
@ -145,7 +146,10 @@
[event] [event]
(when (nil? (.-source event)) (when (nil? (.-source event))
(let [message (.-data 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) (if (:buffer? message)
(rx/push! buffer message) (rx/push! buffer message)
(handle-message message))))) (handle-message message)))))

View file

@ -474,7 +474,7 @@
(->> (rx/from files) (->> (rx/from files)
(rx/mapcat (rx/mapcat
(fn [file] (fn [file]
(->> (rp/command! :export-binfile {:file-id (:id file) (->> (rp/cmd! :export-binfile {:file-id (:id file)
:include-libraries? (= export-type :all) :include-libraries? (= export-type :all)
:embed-assets? (= export-type :merge)}) :embed-assets? (= export-type :merge)})
(rx/map #(hash-map :type :finish (rx/map #(hash-map :type :finish

View file

@ -672,7 +672,7 @@
:response-type :blob :response-type :blob
:method :get}) :method :get})
(rx/map :body) (rx/map :body)
(rx/mapcat #(rp/command! :import-binfile {:file % (rx/mapcat #(rp/cmd! :import-binfile {:file %
:project-id project-id})) :project-id project-id}))
(rx/map (rx/map
(fn [_] (fn [_]

View file

@ -16,6 +16,7 @@
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.core :as rx] [beicon.core :as rx]
[debug :refer [debug?]] [debug :refer [debug?]]
[promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(log/set-level! :trace) (log/set-level! :trace)
@ -110,8 +111,8 @@
(rx/catch body-too-large? (constantly (rx/of nil))) (rx/catch body-too-large? (constantly (rx/of nil)))
(rx/map (constantly params))))) (rx/map (constantly params)))))
(defmethod impl/handler :thumbnails/generate (defmethod impl/handler :thumbnails/generate-for-file
[{:keys [file-id revn features] :as message}] [{:keys [file-id revn features] :as message} _]
(letfn [(on-result [{:keys [data props]}] (letfn [(on-result [{:keys [data props]}]
{:data data {:data data
:fonts (:fonts props)}) :fonts (:fonts props)})
@ -130,3 +131,18 @@
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit"))) (log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "hit")))
(rx/catch not-found? on-cache-miss) (rx/catch not-found? on-cache-miss)
(rx/map on-result))))) (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))))))