Merge pull request #3272 from penpot/azazeln28-thumbnail-renderer

🎉 Add thumbnail renderer service
This commit is contained in:
Alejandro 2023-06-22 13:45:07 +02:00 committed by GitHub
commit 74e8081574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 704 additions and 305 deletions

View file

@ -38,6 +38,11 @@
;; --- FEATURES ;; --- FEATURES
(defn resolve-public-uri
[media-id]
(when media-id
(str (cf/get :public-uri) "/assets/by-id/" media-id)))
(def supported-features (def supported-features
#{"storage/objects-map" #{"storage/objects-map"
"storage/pointer-map" "storage/pointer-map"
@ -413,15 +418,23 @@
f.modified_at, f.modified_at,
f.name, f.name,
f.revn, f.revn,
f.is_shared f.is_shared,
ft.media_id
from file as f from file as f
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where f.project_id = ? where f.project_id = ?
and f.deleted_at is null and f.deleted_at is null
order by f.modified_at desc") order by f.modified_at desc")
(defn get-project-files (defn get-project-files
[conn project-id] [conn project-id]
(db/exec! conn [sql:project-files project-id])) (->> (db/exec! conn [sql:project-files project-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(sv/defmethod ::get-project-files (sv/defmethod ::get-project-files
"Get all files for the specified project." "Get all files for the specified project."
@ -668,9 +681,11 @@
f.modified_at, f.modified_at,
f.name, f.name,
f.is_shared, f.is_shared,
ft.media_id,
row_number() over w as row_num row_number() over w as row_num
from file as f from file as f
join project as p on (p.id = f.project_id) inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
where p.team_id = ? where p.team_id = ?
and p.deleted_at is null and p.deleted_at is null
and f.deleted_at is null and f.deleted_at is null
@ -681,7 +696,13 @@
(defn get-team-recent-files (defn get-team-recent-files
[conn team-id] [conn team-id]
(db/exec! conn [sql:team-recent-files team-id])) (->> (db/exec! conn [sql:team-recent-files team-id])
(mapv (fn [row]
(if-let [media-id (:media-id row)]
(-> row
(dissoc :media-id)
(assoc :thumbnail-uri (resolve-public-uri media-id)))
(dissoc row :media-id))))))
(s/def ::get-team-recent-files (s/def ::get-team-recent-files
(s/keys :req [::rpc/profile-id] (s/keys :req [::rpc/profile-id]

View file

@ -14,7 +14,6 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.types.shape-tree :as ctt] [app.common.types.shape-tree :as ctt]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
@ -39,10 +38,6 @@
;; --- COMMAND QUERY: get-file-object-thumbnails ;; --- COMMAND QUERY: get-file-object-thumbnails
(defn- get-public-uri
[media-id]
(str (cf/get :public-uri) "/assets/by-id/" media-id))
(defn- get-object-thumbnails (defn- get-object-thumbnails
([conn file-id] ([conn file-id]
(let [sql (str/concat (let [sql (str/concat
@ -52,7 +47,7 @@
res (db/exec! conn [sql file-id])] res (db/exec! conn [sql file-id])]
(->> res (->> 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 files/resolve-public-uri)
(:data row)))) (:data row))))
(d/without-nils)))) (d/without-nils))))
@ -65,7 +60,7 @@
res (db/exec! conn [sql file-id ids])] res (db/exec! conn [sql file-id ids])]
(d/index-by :object-id (d/index-by :object-id
(fn [row] (fn [row]
(or (some-> row :media-id get-public-uri) (or (some-> row :media-id files/resolve-public-uri)
(:data row))) (:data row)))
res)))) res))))
@ -85,8 +80,6 @@
;; --- COMMAND QUERY: get-file-thumbnail ;; --- COMMAND QUERY: get-file-thumbnail
;; FIXME: refactor to support uploading data to storage
(defn get-file-thumbnail (defn get-file-thumbnail
[conn file-id revn] [conn file-id revn]
(let [sql (sql/select :file-thumbnail (let [sql (sql/select :file-thumbnail
@ -95,10 +88,15 @@
{:limit 1 {:limit 1
:order-by [[:revn :desc]]}) :order-by [[:revn :desc]]})
row (db/exec-one! conn sql)] row (db/exec-one! conn sql)]
(when-not row (when-not row
(ex/raise :type :not-found (ex/raise :type :not-found
:code :file-thumbnail-not-found)) :code :file-thumbnail-not-found))
(when-not (:data row)
(ex/raise :type :not-found
:code :file-thumbnail-not-found))
{:data (:data row) {:data (:data row)
:props (some-> (:props row) db/decode-transit-pgobject) :props (some-> (:props row) db/decode-transit-pgobject)
:revn (:revn row) :revn (:revn row)
@ -113,20 +111,16 @@
:opt-un [::revn])) :opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail (sv/defmethod ::get-file-thumbnail
"Method used in frontend for obtain the file thumbnail (used in the {::doc/added "1.17"
dashboard)." ::doc/deprecated "1.19"}
{::doc/added "1.17"}
[{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn) (-> (get-file-thumbnail conn file-id revn)
(rph/with-http-cache long-cache-duration)))) (rph/with-http-cache long-cache-duration))))
;; --- COMMAND QUERY: get-file-data-for-thumbnail ;; --- COMMAND QUERY: get-file-data-for-thumbnail
;; FIXME: performance issue, handle new media_id
;;
;; We need to improve how we set frame for thumbnail in order to avoid ;; We need to improve how we set frame for thumbnail in order to avoid
;; loading all pages into memory for find the frame set for thumbnail. ;; loading all pages into memory for find the frame set for thumbnail.
@ -310,14 +304,17 @@
(:id media) (:id media)]))) (:id media) (:id media)])))
(s/def ::media (s/nilable ::media/upload)) (def schema:create-file-object-thumbnail
(s/def ::create-file-object-thumbnail [:map {:title "create-file-object-thumbnail"}
(s/keys :req [::rpc/profile-id] [:file-id ::sm/uuid]
:req-un [::file-id ::object-id ::media])) [:object-id :string]
[:media ::media/upload]])
(sv/defmethod ::create-file-object-thumbnail (sv/defmethod ::create-file-object-thumbnail
{:doc/added "1.19" {:doc/added "1.19"
::audit/skip true} ::audit/skip true
::sm/params schema:create-file-object-thumbnail}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id object-id media]}]
(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)
@ -380,7 +377,6 @@
(db/exec-one! conn [sql:upsert-file-thumbnail (db/exec-one! conn [sql:upsert-file-thumbnail
file-id revn data props data props]))) file-id revn data props data props])))
(s/def ::revn ::us/integer) (s/def ::revn ::us/integer)
(s/def ::props map?) (s/def ::props map?)
@ -427,24 +423,27 @@
:bucket "file-thumbnail"})] :bucket "file-thumbnail"})]
(db/exec-one! conn [sql:create-file-thumbnail file-id revn (db/exec-one! conn [sql:create-file-thumbnail file-id revn
(:id media) props (:id media) props
(:id media) props]))) (:id media) props])
media))
(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 (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.19" {::doc/added "1.19"
::audit/skip true} ::audit/skip true
::sm/params [:map {:title "create-file-thumbnail"}
[:file-id ::sm/uuid]
[:revn :int]
[:media ::media/upload]]
}
[{: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)
(-> cfg (let [media (-> cfg
(update ::sto/storage media/configure-assets-storage) (update ::sto/storage media/configure-assets-storage)
(assoc ::db/conn conn) (assoc ::db/conn conn)
(create-file-thumbnail! params)) (create-file-thumbnail! params))]
nil)))
{:uri (files/resolve-public-uri (:id media))}))))

View file

@ -184,7 +184,7 @@
(when (seq res) (when (seq res)
(doseq [media-id (into #{} (keep :media-id) res)] (doseq [media-id (into #{} (keep :media-id) res)]
;; Mark as deleted the storage object related with the ;; Mark as deleted the storage object related with the
;; photo-id field. ;; media-id field.
(l/trace :hint "mark storage object as deleted" :id media-id) (l/trace :hint "mark storage object as deleted" :id media-id)
(sto/del-object! storage media-id)) (sto/del-object! storage media-id))

View file

@ -141,7 +141,7 @@
))) )))
(t/deftest upsert-file-thumbnail (t/deftest create-file-thumbnail
(let [storage (::sto/storage th/*system*) (let [storage (::sto/storage th/*system*)
profile (th/create-profile* 1) profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile) file (th/create-file* 1 {:profile-id (:id profile)
@ -159,7 +159,6 @@
data2 {::th/type :create-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 {}
:revn 2 :revn 2
:media {:filename "sample.jpg" :media {:filename "sample.jpg"
:size 7923 :size 7923
@ -169,7 +168,6 @@
data3 {::th/type :create-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 {}
:revn 3 :revn 3
:media {:filename "sample.jpg" :media {:filename "sample.jpg"
:size 312043 :size 312043
@ -183,11 +181,11 @@
(let [out (th/command! data2)] (let [out (th/command! data2)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) (t/is (contains? (:result out) :uri)))
(let [out (th/command! data3)] (let [out (th/command! data3)]
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) (t/is (contains? (:result out) :uri)))
(let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail (let [[row1 row2 row3 :as rows] (th/db-query :file-thumbnail
{:file-id (:id file)} {:file-id (:id file)}

View file

@ -131,7 +131,8 @@ function readManifest() {
"polyfills": "js/polyfills.js", "polyfills": "js/polyfills.js",
"main": "js/main.js", "main": "js/main.js",
"shared": "js/shared.js", "shared": "js/shared.js",
"worker": "js/worker.js" "worker": "js/worker.js",
"thumbnail-renderer": "js/thumbnail-renderer.js"
}; };
} }
} }
@ -242,7 +243,17 @@ gulp.task("template:render", templatePipeline({
output: paths.output output: paths.output
})); }));
gulp.task("templates", gulp.series("svg:sprite:icons", "svg:sprite:cursors", "template:main", "template:render")); gulp.task("template:thumbnail-renderer", templatePipeline({
name: "thumbnail-renderer.html",
input: paths.resources + "templates/thumbnail-renderer.mustache",
output: paths.output
}));
gulp.task("templates", gulp.series("svg:sprite:icons",
"svg:sprite:cursors",
"template:main",
"template:render",
"template:thumbnail-renderer"));
gulp.task("polyfills", function() { gulp.task("polyfills", function() {
return gulp.src(paths.resources + "polyfills/*.js") return gulp.src(paths.resources + "polyfills/*.js")

View file

@ -51,6 +51,10 @@
border-radius: $br3; border-radius: $br3;
border: 2px solid lighten($color-gray-20, 15%); border: 2px solid lighten($color-gray-20, 15%);
text-align: initial; text-align: initial;
img {
object-fit: contain;
}
} }
&.dragged { &.dragged {

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Penpot - Thumbnail Renderer</title>
<link rel="icon" href="images/favicon.png" />
<script>
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
</script>
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker}}"</script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
{{/manifest}}
</head>
<body>
{{# manifest}}
<script src="{{& shared}}"></script>
<script src="{{& thumbnail-renderer}}"></script>
{{/manifest}}
</body>
</html>

View file

@ -16,17 +16,25 @@
:modules :modules
{:shared {:entries []} {:shared {:entries []}
:main {:entries [app.main] :main
{:entries [app.main]
:depends-on #{:shared} :depends-on #{:shared}
:init-fn app.main/init} :init-fn app.main/init}
:render {:entries [app.render] :render
{:entries [app.render]
:depends-on #{:shared} :depends-on #{:shared}
:init-fn app.render/init} :init-fn app.render/init}
:worker {:entries [app.worker] :worker
{:entries [app.worker]
:web-worker true :web-worker true
:depends-on #{:shared}}} :depends-on #{:shared}}
:thumbnail-renderer
{:entries [app.thumbnail-renderer]
:depends-on #{:shared}
:init-fn app.thumbnail-renderer/init}}
:compiler-options :compiler-options
{:output-feature-set :es2020 {:output-feature-set :es2020

View file

@ -79,21 +79,21 @@
"unknown" "unknown"
date))) date)))
;; --- Globar Config Vars ;; --- Globar Config Vars
(def default-theme "default") (def default-theme "default")
(def default-language "en") (def default-language "en")
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations")) (def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
(def build-date (parse-build-date global)) (def build-date (parse-build-date global))
(def flags (atom (parse-flags global))) (def flags (parse-flags global))
(def version (atom (parse-version global))) (def version (parse-version global))
(def target (atom (parse-target global))) (def target (parse-target global))
(def browser (atom (parse-browser))) (def browser (parse-browser))
(def platform (atom (parse-platform))) (def platform (parse-platform))
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil)) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil)) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
@ -108,36 +108,43 @@
(update :path #(str % "/"))))) (update :path #(str % "/")))))
(def public-uri (def public-uri
(atom
(normalize-uri (or (obj/get global "penpotPublicURI") (normalize-uri (or (obj/get global "penpotPublicURI")
(.-origin ^js location))))) (obj/get location "origin"))))
(def thumbnail-renderer-uri
(or (some-> (obj/get global "penpotThumbnailRendererURI") normalize-uri)
public-uri))
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
;; --- Helper Functions ;; --- Helper Functions
(defn ^boolean check-browser? [candidate] (defn ^boolean check-browser? [candidate]
(dm/assert! (contains? valid-browsers candidate)) (dm/assert! (contains? valid-browsers candidate))
(= candidate @browser)) (= candidate browser))
(defn ^boolean check-platform? [candidate] (defn ^boolean check-platform? [candidate]
(dm/assert! (contains? valid-platforms candidate)) (dm/assert! (contains? valid-platforms candidate))
(= candidate @platform)) (= candidate platform))
(defn resolve-profile-photo-url (defn resolve-profile-photo-url
[{:keys [photo-id fullname name] :as profile}] [{:keys [photo-id fullname name] :as profile}]
(if (nil? photo-id) (if (nil? photo-id)
(avatars/generate {:name (or fullname name)}) (avatars/generate {:name (or fullname name)})
(str (u/join @public-uri "assets/by-id/" photo-id)))) (dm/str (u/join public-uri "assets/by-id/" photo-id))))
(defn resolve-team-photo-url (defn resolve-team-photo-url
[{:keys [photo-id name] :as team}] [{:keys [photo-id name] :as team}]
(if (nil? photo-id) (if (nil? photo-id)
(avatars/generate {:name name}) (avatars/generate {:name name})
(str (u/join @public-uri "assets/by-id/" photo-id)))) (dm/str (u/join public-uri "assets/by-id/" photo-id))))
(defn resolve-file-media (defn resolve-file-media
([media] ([media]
(resolve-file-media media false)) (resolve-file-media media false))
([{:keys [id] :as media} thumbnail?] ([{:keys [id] :as media} thumbnail?]
(str (cond-> (u/join @public-uri "assets/by-file-media-id/") (dm/str
(true? thumbnail?) (u/join (str id "/thumbnail")) (cond-> (u/join public-uri "assets/by-file-media-id/")
(false? thumbnail?) (u/join (str id)))))) (true? thumbnail?) (u/join (dm/str id "/thumbnail"))
(false? thumbnail?) (u/join (dm/str id))))))

View file

@ -6,6 +6,7 @@
(ns app.main (ns app.main
(:require (:require
[app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
@ -15,6 +16,7 @@
[app.main.errors] [app.main.errors]
[app.main.features :as feat] [app.main.features :as feat]
[app.main.store :as st] [app.main.store :as st]
[app.main.thumbnail-renderer :as tr]
[app.main.ui :as ui] [app.main.ui :as ui]
[app.main.ui.alert] [app.main.ui.alert]
[app.main.ui.confirm] [app.main.ui.confirm]
@ -34,12 +36,12 @@
(log/setup! {:app :info}) (log/setup! {:app :info})
(when (= :browser @cf/target) (when (= :browser cf/target)
(log/info :message "Welcome to penpot" (log/info :message "Welcome to penpot"
:version (:full @cf/version) :version (:full cf/version)
:asserts *assert* :asserts *assert*
:build-date cf/build-date :build-date cf/build-date
:public-uri (str @cf/public-uri))) :public-uri (dm/str cf/public-uri)))
(declare reinit) (declare reinit)
@ -80,6 +82,7 @@
(i18n/init! cf/translations) (i18n/init! cf/translations)
(theme/init! cf/themes) (theme/init! cf/themes)
(cur/init-styles) (cur/init-styles)
(tr/init!)
(init-ui) (init-ui)
(st/emit! (initialize))) (st/emit! (initialize)))

View file

@ -475,7 +475,7 @@
(rx/map (fn [params] (rx/map (fn [params]
(rt/resolve router :auth-verify-token {} params))) (rt/resolve router :auth-verify-token {} params)))
(rx/map (fn [fragment] (rx/map (fn [fragment]
(assoc @cf/public-uri :fragment fragment))) (assoc cf/public-uri :fragment fragment)))
(rx/tap (fn [uri] (rx/tap (fn [uri]
(wapi/write-to-clipboard (str uri)))) (wapi/write-to-clipboard (str uri))))
(rx/tap on-success) (rx/tap on-success)
@ -782,6 +782,15 @@
(->> (rp/cmd! :set-file-shared params) (->> (rp/cmd! :set-file-shared params)
(rx/ignore)))))) (rx/ignore))))))
(defn set-file-thumbnail
[file-id thumbnail-uri]
(ptk/reify ::set-file-thumbnail
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri)
(d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri)))))
;; --- EVENT: create-file ;; --- EVENT: create-file
(declare file-created) (declare file-created)

View file

@ -39,7 +39,7 @@
[] []
(let [uagent (UAParser.)] (let [uagent (UAParser.)]
(merge (merge
{:app-version (:full @cf/version) {:app-version (:full cf/version)
:locale @i18n/locale} :locale @i18n/locale}
(let [browser (.getBrowser uagent)] (let [browser (.getBrowser uagent)]
{:browser (obj/get browser "name") {:browser (obj/get browser "name")
@ -215,7 +215,7 @@
(defn- persist-events (defn- persist-events
[events] [events]
(if (seq events) (if (seq events)
(let [uri (u/join @cf/public-uri "api/rpc/command/push-audit-events") (let [uri (u/join cf/public-uri "api/rpc/command/push-audit-events")
params {:uri uri params {:uri uri
:method :post :method :post
:credentials "include" :credentials "include"
@ -230,7 +230,7 @@
(defn initialize (defn initialize
[] []
(when (contains? @cf/flags :audit-log) (when (contains? cf/flags :audit-log)
(ptk/reify ::initialize (ptk/reify ::initialize
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ stream] (effect [_ _ stream]

View file

@ -377,7 +377,7 @@
(ptk/reify ::mark-onboarding-as-viewed (ptk/reify ::mark-onboarding-as-viewed
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [version (or version (:main @cf/version)) (let [version (or version (:main cf/version))
props {:onboarding-viewed true props {:onboarding-viewed true
:release-notes-viewed version}] :release-notes-viewed version}]
(->> (rp/cmd! :update-profile-props {:props props}) (->> (rp/cmd! :update-profile-props {:props props})

View file

@ -22,7 +22,7 @@
(defn- prepare-uri (defn- prepare-uri
[params] [params]
(let [base (-> @cf/public-uri (let [base (-> cf/public-uri
(u/join "ws/notifications") (u/join "ws/notifications")
(assoc :query (u/map->query-string params)))] (assoc :query (u/map->query-string params)))]
(cond-> base (cond-> base

View file

@ -87,7 +87,7 @@
(log/trace :hint "event:initialize" :fn "features") (log/trace :hint "event:initialize" :fn "features")
(rx/concat (rx/concat
;; Enable all features set on the configuration ;; Enable all features set on the configuration
(->> (rx/from @cf/flags) (->> (rx/from cf/flags)
(rx/map name) (rx/map name)
(rx/map (fn [flag] (rx/map (fn [flag]
(when (str/starts-with? flag "frontend-feature-") (when (str/starts-with? flag "frontend-feature-")

View file

@ -9,6 +9,7 @@
(:require-macros [app.main.fonts :refer [preload-gfonts]]) (:require-macros [app.main.fonts :refer [preload-gfonts]])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.text :as txt] [app.common.text :as txt]
[app.config :as cf] [app.config :as cf]
@ -81,8 +82,12 @@
;; FONTS LOADING ;; FONTS LOADING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defonce loaded (l/atom #{})) (defonce ^:dynamic loaded (l/atom #{}))
(defonce loading (l/atom {})) (defonce ^:dynamic loading (l/atom {}))
;; NOTE: mainly used on worker, when you don't really need load font
;; only know if the font is needed or not
(defonce ^:dynamic loaded-hints (l/atom #{}))
(defn- create-link-element (defn- create-link-element
[uri] [uri]
@ -148,31 +153,26 @@
;; --- LOADER: CUSTOM ;; --- LOADER: CUSTOM
(def font-css-template (def font-face-template
"@font-face { "@font-face {
font-family: '%(family)s'; font-family: '%(family)s';
font-style: %(style)s; font-style: %(style)s;
font-weight: %(weight)s; font-weight: %(weight)s;
font-display: block; font-display: block;
src: url(%(woff1-uri)s) format('woff'), src: url(%(uri)s) format('woff');
url(%(ttf-uri)s) format('ttf'),
url(%(otf-uri)s) format('otf');
}") }")
(defn- asset-id->uri (defn- asset-id->uri
[asset-id] [asset-id]
(str (u/join @cf/public-uri "assets/by-id/" asset-id))) (str (u/join cf/public-uri "assets/by-id/" asset-id)))
(defn generate-custom-font-variant-css (defn generate-custom-font-variant-css
[family variant] [family variant]
(str/fmt font-css-template (str/fmt font-face-template
{:family family {:family family
:style (:style variant) :style (:style variant)
:weight (:weight variant) :weight (:weight variant)
:woff2-uri (asset-id->uri (::woff2-file-id variant)) :uri (asset-id->uri (::woff1-file-id variant))}))
:woff1-uri (asset-id->uri (::woff1-file-id variant))
:ttf-uri (asset-id->uri (::ttf-file-id variant))
:otf-uri (asset-id->uri (::otf-file-id variant))}))
(defn- generate-custom-font-css (defn- generate-custom-font-css
[{:keys [family variants] :as font}] [{:keys [family variants] :as font}]
@ -194,34 +194,35 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn ensure-loaded! (defn ensure-loaded!
[id] ([font-id] (ensure-loaded! font-id nil))
(log/debug :action "try-ensure-loaded!" :font-id id) ([font-id variant-id]
(log/debug :action "try-ensure-loaded!" :font-id font-id :variant-id variant-id)
(if-not (exists? js/window) (if-not (exists? js/window)
;; If we are in the worker environment, we just mark it as loaded ;; If we are in the worker environment, we just mark it as loaded
;; without really loading it. ;; without really loading it.
(do (do
(swap! loaded conj id) (swap! loaded-hints conj {:font-id font-id :font-variant-id variant-id})
(p/resolved id)) (p/resolved font-id))
(let [font (get @fontsdb id)] (let [font (get @fontsdb font-id)]
(cond (cond
(nil? font) (nil? font)
(p/resolved id) (p/resolved font-id)
;; Font already loaded, we just continue ;; Font already loaded, we just continue
(contains? @loaded id) (contains? @loaded font-id)
(p/resolved id) (p/resolved font-id)
;; Font is currently downloading. We attach the caller to the promise ;; Font is currently downloading. We attach the caller to the promise
(contains? @loading id) (contains? @loading font-id)
(p/resolved (get @loading id)) (p/resolved (get @loading font-id))
;; First caller, we create the promise and then wait ;; First caller, we create the promise and then wait
:else :else
(let [on-load (fn [resolve] (let [on-load (fn [resolve]
(swap! loaded conj id) (swap! loaded conj font-id)
(swap! loading dissoc id) (swap! loading dissoc font-id)
(resolve id)) (resolve font-id))
load-p (p/create load-p (p/create
(fn [resolve _] (fn [resolve _]
@ -229,34 +230,27 @@
(assoc ::on-loaded (partial on-load resolve)) (assoc ::on-loaded (partial on-load resolve))
(load-font))))] (load-font))))]
(swap! loading assoc id load-p) (swap! loading assoc font-id load-p)
load-p))))) load-p))))))
(defn ready (defn ready
[cb] [cb]
(-> (obj/get-in js/document ["fonts" "ready"]) (-> (obj/get-in js/document ["fonts" "ready"])
(p/then cb))) (p/then cb)))
(defn get-default-variant [{:keys [variants]}] (defn get-default-variant
(or [{:keys [variants]}]
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants) (or (d/seek #(or (= (:id %) "regular")
(= (:name %) "regular")) variants)
(first variants))) (first variants)))
(defn get-variant
[{:keys [variants] :as font} font-variant-id]
(or (d/seek #(= (:id %) font-variant-id) variants)
(get-default-variant font)))
;; Font embedding functions ;; Font embedding functions
;; Template for a CSS font face
(def font-face-template "
/* latin */
@font-face {
font-family: '%(family)s';
font-style: %(style)s;
font-weight: %(weight)s;
font-display: block;
src: url(%(baseurl)sfonts/%(family)s-%(suffix)s.woff) format('woff');
}
")
(defn get-content-fonts (defn get-content-fonts
"Extracts the fonts used by the content of a text shape" "Extracts the fonts used by the content of a text shape"
[{font-id :font-id children :children :as content}] [{font-id :font-id children :children :as content}]
@ -267,38 +261,45 @@
children-font (->> children (mapv get-content-fonts))] children-font (->> children (mapv get-content-fonts))]
(reduce set/union (conj children-font current-font)))) (reduce set/union (conj children-font current-font))))
(defn fetch-font-css (defn fetch-font-css
"Given a font and the variant-id, retrieves the fontface CSS" "Given a font and the variant-id, retrieves the fontface CSS"
[{:keys [font-id font-variant-id] [{:keys [font-id font-variant-id]
:or {font-variant-id "regular"}}] :or {font-variant-id "regular"}}]
(let [{:keys [backend family variants]} (get @fontsdb font-id)] (let [{:keys [backend family] :as font} (get @fontsdb font-id)]
(cond (cond
(nil? font)
(rx/empty)
(= :google backend) (= :google backend)
(let [variant (d/seek #(= (:id %) font-variant-id) variants)] (let [variant (get-variant font font-variant-id)]
(-> (generate-gfonts-url (-> (generate-gfonts-url
{:family family {:family family
:variants [variant]}) :variants [variant]})
(http/fetch-text))) (http/fetch-text)))
(= :custom backend) (= :custom backend)
(let [variant (d/seek #(= (:id %) font-variant-id) variants) (let [variant (get-variant font font-variant-id)
result (generate-custom-font-variant-css family variant)] result (generate-custom-font-variant-css family variant)]
(p/resolved result)) (rx/of result))
:else :else
(let [{:keys [weight style suffix] :as variant} (let [{:keys [weight style suffix]} (get-variant font font-variant-id)
(d/seek #(= (:id %) font-variant-id) variants) suffix (or suffix font-variant-id)
font-data {:baseurl (str @cf/public-uri) params {:uri (dm/str cf/public-uri "fonts/" family "-" suffix ".woff")
:family family :family family
:style style :style style
:suffix (or suffix font-variant-id)
:weight weight}] :weight weight}]
(rx/of (str/fmt font-face-template font-data)))))) (rx/of (str/fmt font-face-template params))))))
(defn extract-fontface-urls (defn extract-fontface-urls
"Parses the CSS and retrieves the font urls" "Parses the CSS and retrieves the font urls"
[^string css] [^string css]
(->> (re-seq #"url\(([^)]+)\)" css) (->> (re-seq #"url\(([^)]+)\)" css)
(mapv second))) (mapv second)))
(defn render-font-styles
[font-refs]
(->> (rx/from font-refs)
(rx/mapcat fetch-font-css)
(rx/reduce (fn [acc css] (dm/str acc "\n" css)) "")))

View file

@ -50,6 +50,11 @@
:upsert-file-object-thumbnail {:query-params [:file-id :object-id]} :upsert-file-object-thumbnail {:query-params [:file-id :object-id]}
:create-file-object-thumbnail {:query-params [:file-id :object-id] :create-file-object-thumbnail {:query-params [:file-id :object-id]
:form-data? true} :form-data? true}
:create-file-thumbnail
{:query-params [:file-id :revn]
:form-data? true}
:export-binfile {:response-type :blob} :export-binfile {:response-type :blob}
:import-binfile {:form-data? true} :import-binfile {:form-data? true}
:retrieve-list-of-builtin-templates {:query-params :all} :retrieve-list-of-builtin-templates {:query-params :all}
@ -79,7 +84,7 @@
:else :post) :else :post)
request {:method method 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"}
:body (when (= method :post) :body (when (= method :post)
@ -105,7 +110,7 @@
(defmethod cmd! :login-with-oidc (defmethod cmd! :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)]
(->> (http/send! {:method :post (->> (http/send! {:method :post
:uri uri :uri uri
@ -117,7 +122,7 @@
(defn- send-export (defn- send-export
[{:keys [blob?] :as params}] [{:keys [blob?] :as params}]
(->> (http/send! {:method :post (->> (http/send! {:method :post
:uri (u/join @cf/public-uri "api/export") :uri (u/join cf/public-uri "api/export")
:body (http/transit-data (dissoc params :blob?)) :body (http/transit-data (dissoc params :blob?))
:credentials "include" :credentials "include"
:response-type (if blob? :blob :text)}) :response-type (if blob? :blob :text)})
@ -136,7 +141,7 @@
(defmethod cmd! ::multipart-upload (defmethod cmd! ::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))
:credentials "include" :credentials "include"
:body (http/form-data params)}) :body (http/form-data params)})
(rx/map http/conditional-decode-transit) (rx/map http/conditional-decode-transit)

View file

@ -0,0 +1,93 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.thumbnail-renderer
"A main entry point for the thumbnail renderer API interface.
This ns is responsible to provide an API for create thumbnail
renderer iframes and interact with them using asyncrhonous
messages."
(:require
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.util.dom :as dom]
[beicon.core :as rx]
[cuerdas.core :as str]))
(defonce ready? false)
(defonce queue #js [])
(defonce instance nil)
(defonce msgbus (rx/subject))
(defonce origin
(dm/str (assoc cf/thumbnail-renderer-uri :path "/thumbnail-renderer.html")))
(declare send-message!)
(defn- process-queued-messages!
[]
(loop [message (.shift ^js queue)]
(when (some? message)
(send-message! message)
(recur (.shift ^js queue)))))
(defn- on-message
"Handles a message from the thumbnail renderer."
[event]
(let [evorigin (unchecked-get event "origin")
evdata (unchecked-get event "data")]
(when (and (object? evdata) (str/starts-with? origin evorigin))
(let [scope (unchecked-get evdata "scope")
type (unchecked-get evdata "type")]
(when (= "penpot/thumbnail-renderer" scope)
(when (= type "ready")
(set! ready? true)
(process-queued-messages!))
(rx/push! msgbus evdata))))))
(defn- send-message!
"Sends a message to the thumbnail renderer."
[message]
(let [window (.-contentWindow ^js instance)]
(.postMessage ^js window message origin)))
(defn- queue-message!
"Queues a message to be sent to the thumbnail renderer when it's ready."
[message]
(.push ^js queue message))
(defn render
"Renders a thumbnail."
[{:keys [data styles] :as params}]
(let [id (dm/str (uuid/next))
payload #js {:data data :styles styles}
message #js {:id id
:scope "penpot/thumbnail-renderer"
:payload payload}]
(if ^boolean ready?
(send-message! message)
(queue-message! message))
(->> msgbus
(rx/filter #(= id (unchecked-get % "id")))
(rx/mapcat (fn [msg]
(case (unchecked-get msg "type")
"success" (rx/of (unchecked-get msg "payload"))
"failure" (rx/throw (unchecked-get msg "payload")))))
(rx/take 1))))
(defn init!
"Initializes the thumbnail renderer."
[]
(let [iframe (dom/create-element "iframe")]
(dom/set-attribute! iframe "src" origin)
(dom/set-attribute! iframe "hidden" true)
(dom/append-child! js/document.body iframe)
(set! instance iframe)
(.addEventListener js/window "message" on-message)))

View file

@ -98,9 +98,9 @@
[:& app.main.ui.onboarding/onboarding-modal {}] [:& app.main.ui.onboarding/onboarding-modal {}]
(and (:onboarding-viewed props) (and (:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main @cf/version)) (not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main @cf/version))) (not= "0.0" (:main cf/version)))
[:& app.main.ui.releases/release-notes-modal {:version (:main @cf/version)}])) [:& app.main.ui.releases/release-notes-modal {:version (:main cf/version)}]))
[:& dashboard {:route route :profile profile}]] [:& dashboard {:route route :profile profile}]]

View file

@ -28,7 +28,7 @@
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def show-alt-login-buttons? (def show-alt-login-buttons?
(some (partial contains? @cf/flags) (some (partial contains? cf/flags)
[:login-with-google [:login-with-google
:login-with-github :login-with-github
:login-with-gitlab :login-with-gitlab
@ -175,13 +175,13 @@
:label (tr "auth.password")}]] :label (tr "auth.password")}]]
[:div.buttons-stack [:div.buttons-stack
(when (or (contains? @cf/flags :login) (when (or (contains? cf/flags :login)
(contains? @cf/flags :login-with-password)) (contains? cf/flags :login-with-password))
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-submit") {:label (tr "auth.login-submit")
:data-test "login-submit"}]) :data-test "login-submit"}])
(when (contains? @cf/flags :login-with-ldap) (when (contains? cf/flags :login-with-ldap)
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-with-ldap-submit") {:label (tr "auth.login-with-ldap-submit")
:on-click on-submit-ldap}])]]])) :on-click on-submit-ldap}])]]]))
@ -189,25 +189,25 @@
(mf/defc login-buttons (mf/defc login-buttons
[{:keys [params] :as props}] [{:keys [params] :as props}]
[:div.auth-buttons [:div.auth-buttons
(when (contains? @cf/flags :login-with-google) (when (contains? cf/flags :login-with-google)
[:& bl/button-link {:action #(login-with-oidc % :google params) [:& bl/button-link {:action #(login-with-oidc % :google params)
:icon i/brand-google :icon i/brand-google
:name (tr "auth.login-with-google-submit") :name (tr "auth.login-with-google-submit")
:klass "btn-google-auth"}]) :klass "btn-google-auth"}])
(when (contains? @cf/flags :login-with-github) (when (contains? cf/flags :login-with-github)
[:& bl/button-link {:action #(login-with-oidc % :github params) [:& bl/button-link {:action #(login-with-oidc % :github params)
:icon i/brand-github :icon i/brand-github
:name (tr "auth.login-with-github-submit") :name (tr "auth.login-with-github-submit")
:klass "btn-github-auth"}]) :klass "btn-github-auth"}])
(when (contains? @cf/flags :login-with-gitlab) (when (contains? cf/flags :login-with-gitlab)
[:& bl/button-link {:action #(login-with-oidc % :gitlab params) [:& bl/button-link {:action #(login-with-oidc % :gitlab params)
:icon i/brand-gitlab :icon i/brand-gitlab
:name (tr "auth.login-with-gitlab-submit") :name (tr "auth.login-with-gitlab-submit")
:klass "btn-gitlab-auth"}]) :klass "btn-gitlab-auth"}])
(when (contains? @cf/flags :login-with-oidc) (when (contains? cf/flags :login-with-oidc)
[:& bl/button-link {:action #(login-with-oidc % :oidc params) [:& bl/button-link {:action #(login-with-oidc % :oidc params)
:icon i/brand-openid :icon i/brand-openid
:name (tr "auth.login-with-oidc-submit") :name (tr "auth.login-with-oidc-submit")
@ -215,7 +215,7 @@
(mf/defc login-button-oidc (mf/defc login-button-oidc
[{:keys [params] :as props}] [{:keys [params] :as props}]
(when (contains? @cf/flags :login-with-oidc) (when (contains? cf/flags :login-with-oidc)
[:div.link-entry.link-oidc [:div.link-entry.link-oidc
[:a {:tab-index "0" [:a {:tab-index "0"
:on-key-down (fn [event] :on-key-down (fn [event]
@ -236,17 +236,17 @@
[:& login-buttons {:params params}] [:& login-buttons {:params params}]
(when (or (contains? @cf/flags :login) (when (or (contains? cf/flags :login)
(contains? @cf/flags :login-with-password) (contains? cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap)) (contains? cf/flags :login-with-ldap))
[:span.separator [:span.separator
[:span.line] [:span.line]
[:span.text (tr "labels.or")] [:span.text (tr "labels.or")]
[:span.line]])]) [:span.line]])])
(when (or (contains? @cf/flags :login) (when (or (contains? cf/flags :login)
(contains? @cf/flags :login-with-password) (contains? cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap)) (contains? cf/flags :login-with-ldap))
[:& login-form {:params params :on-success-callback on-success-callback}])]) [:& login-form {:params params :on-success-callback on-success-callback}])])
(mf/defc login-page (mf/defc login-page
@ -258,21 +258,21 @@
[:& login-methods {:params params}] [:& login-methods {:params params}]
[:div.links [:div.links
(when (or (contains? @cf/flags :login) (when (or (contains? cf/flags :login)
(contains? @cf/flags :login-with-password)) (contains? cf/flags :login-with-password))
[:div.link-entry [:div.link-entry
[:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request))
:data-test "forgot-password"} :data-test "forgot-password"}
(tr "auth.forgot-password")]]) (tr "auth.forgot-password")]])
(when (contains? @cf/flags :registration) (when (contains? cf/flags :registration)
[:div.link-entry [:div.link-entry
[:span (tr "auth.register") " "] [:span (tr "auth.register") " "]
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params)) [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params))
:data-test "register-submit"} :data-test "register-submit"}
(tr "auth.register-submit")]])] (tr "auth.register-submit")]])]
(when (contains? @cf/flags :demo-users) (when (contains? cf/flags :demo-users)
[:div.links.demo [:div.links.demo
[:div.link-entry [:div.link-entry
[:span (tr "auth.create-demo-profile") " "] [:span (tr "auth.create-demo-profile") " "]

View file

@ -141,8 +141,8 @@
[:& login/login-buttons {:params params}] [:& login/login-buttons {:params params}]
(when (or (contains? @cf/flags :login) (when (or (contains? cf/flags :login)
(contains? @cf/flags :login-with-ldap)) (contains? cf/flags :login-with-ldap))
[:span.separator [:span.separator
[:span.line] [:span.line]
[:span.text (tr "labels.or")] [:span.text (tr "labels.or")]
@ -157,7 +157,7 @@
[:h1 {:data-test "registration-title"} (tr "auth.register-title")] [:h1 {:data-test "registration-title"} (tr "auth.register-title")]
[:div.subtitle (tr "auth.register-subtitle")] [:div.subtitle (tr "auth.register-subtitle")]
(when (contains? @cf/flags :demo-warning) (when (contains? cf/flags :demo-warning)
[:& demo-warning]) [:& demo-warning])
[:& register-methods {:params params}] [:& register-methods {:params params}]
@ -170,7 +170,7 @@
:data-test "login-here-link"} :data-test "login-here-link"}
(tr "auth.login-here")]] (tr "auth.login-here")]]
(when (contains? @cf/flags :demo-users) (when (contains? cf/flags :demo-users)
[:div.link-entry [:div.link-entry
[:span (tr "auth.create-demo-profile") " "] [:span (tr "auth.create-demo-profile") " "]
[:& lk/link {:action #(st/emit! (du/create-demo-profile))} [:& lk/link {:action #(st/emit! (du/create-demo-profile))}
@ -207,7 +207,7 @@
(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) (s/def ::accept-terms-and-privacy (s/and ::us/boolean true?))
(s/def ::accept-newsletter-subscription ::us/boolean) (s/def ::accept-newsletter-subscription ::us/boolean)
(if (contains? @cf/flags :terms-and-privacy-checkbox) (if (contains? cf/flags :terms-and-privacy-checkbox)
(s/def ::register-validate-form (s/def ::register-validate-form
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
:opt-un [::accept-newsletter-subscription])) :opt-un [::accept-newsletter-subscription]))
@ -244,7 +244,7 @@
:label (tr "auth.fullname") :label (tr "auth.fullname")
:type "text"}]] :type "text"}]]
(when (contains? @cf/flags :terms-and-privacy-checkbox) (when (contains? cf/flags :terms-and-privacy-checkbox)
[:div.fields-row.input-visible.accept-terms-and-privacy-wrapper [:div.fields-row.input-visible.accept-terms-and-privacy-wrapper
[:& fm/input {:name :accept-terms-and-privacy [:& fm/input {:name :accept-terms-and-privacy
:class "check-primary" :class "check-primary"

View file

@ -16,7 +16,9 @@
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.render :refer [component-svg]] [app.main.render :refer [component-svg]]
[app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.thumbnail-renderer :as thr]
[app.main.ui.components.color-bullet :as bc] [app.main.ui.components.color-bullet :as bc]
[app.main.ui.dashboard.file-menu :refer [file-menu]] [app.main.ui.dashboard.file-menu :refer [file-menu]]
[app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.import :refer [use-import-file]]
@ -30,7 +32,6 @@
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.perf :as perf]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.timers :as ts] [app.util.timers :as ts]
[beicon.core :as rx] [beicon.core :as rx]
@ -41,44 +42,49 @@
;; --- Grid Item Thumbnail ;; --- Grid Item Thumbnail
(defn ask-for-thumbnail (defn- persist-thumbnail
[file-id revn blob]
(let [params {:file-id file-id :revn revn :media blob}]
(->> (rp/cmd! :create-file-thumbnail params)
(rx/map :uri))))
(defn- ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache" "Creates some hooks to handle the files thumbnails cache"
[file] [file-id revn]
(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-for-file (->> (wrk/ask! {:cmd :thumbnails/generate-for-file
:revn (:revn file) :revn revn
:file-id (:id file) :file-id file-id
:file-name (:name file) :features features})
:features features}))) (rx/mapcat (fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(assoc result :styles styles))))))
(rx/mapcat thr/render)
(rx/mapcat (partial persist-thumbnail file-id revn)))))
(mf/defc grid-item-thumbnail (mf/defc grid-item-thumbnail
{::mf/wrap [mf/memo]} {::mf/wrap-props false}
[{:keys [file] :as props}] [{:keys [file-id revn thumbnail-uri background-color]}]
(let [container (mf/use-ref) (let [container (mf/use-ref)
bgcolor (dm/get-in file [:data :options :background])
visible? (h/use-visible container :once? true)] visible? (h/use-visible container :once? true)]
(mf/with-effect [file visible?] (mf/with-effect [file-id revn visible? thumbnail-uri]
(when visible? (when (and visible? (not thumbnail-uri))
(let [tp (perf/tpoint)] (->> (ask-for-thumbnail file-id revn)
(->> (ask-for-thumbnail file) (rx/subs (fn [url]
(rx/subscribe-on :af) (st/emit! (dd/set-file-thumbnail file-id url)))))))
(rx/subs (fn [{:keys [data fonts] :as params}]
(run! fonts/ensure-loaded! fonts)
(log/debug :hint "loaded thumbnail"
:file-id (dm/str (:id file))
:file-name (:name file)
:elapsed (str/ffmt "%ms" (tp)))
(when-let [node (mf/ref-val container)]
(dom/set-html! node data))))))))
[:div.grid-item-th [:div.grid-item-th
{:style {:background-color bgcolor} {:style {:background-color background-color}
:ref container} :ref container}
i/loader-pencil])) (when visible?
(if thumbnail-uri
[:img.grid-item-thumbnail-image {:src thumbnail-uri}]
i/loader-pencil))]))
;; --- Grid Item Library ;; --- Grid Item Library
@ -312,7 +318,12 @@
[:div.overlay] [:div.overlay]
(if library-view? (if library-view?
[:& grid-item-library {:file file}] [:& grid-item-library {:file file}]
[:& grid-item-thumbnail {:file file}]) [:& grid-item-thumbnail
{:file-id (:id file)
:revn (:revn file)
:thumbnail-uri (:thumbnail-uri file)
:background-color (dm/get-in file [:data :options :background])}])
(when (and (:is-shared file) (not library-view?)) (when (and (:is-shared file) (not library-view?))
[:div.item-badge i/library]) [:div.item-badge i/library])
[:div.info-wrapper [:div.info-wrapper

View file

@ -376,7 +376,7 @@
:data-test "team-invitations"} :data-test "team-invitations"}
(tr "labels.invitations")] (tr "labels.invitations")]
(when (contains? @cf/flags :webhooks) (when (contains? cf/flags :webhooks)
[:& dropdown-menu-item {:on-click go-webhooks [:& dropdown-menu-item {:on-click go-webhooks
:on-key-down (fn [event] :on-key-down (fn [event]
(when (kbd/enter? event) (when (kbd/enter? event)
@ -459,7 +459,7 @@
can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin]))
options-ids ["teams-options-members" options-ids ["teams-options-members"
"teams-options-invitations" "teams-options-invitations"
(when (contains? @cf/flags :webhooks) (when (contains? cf/flags :webhooks)
"teams-options-webhooks") "teams-options-webhooks")
"teams-options-settings" "teams-options-settings"
(when can-rename? (when can-rename?
@ -680,7 +680,7 @@
show-release-notes show-release-notes
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
(let [version (:main @cf/version)] (let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event)) (if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding})) (st/emit! (modal/show {:type :onboarding}))
@ -769,7 +769,7 @@
(dom/open-new-window "https://penpot.app/terms")))} (dom/open-new-window "https://penpot.app/terms")))}
[:span (tr "auth.terms-of-service")]] [:span (tr "auth.terms-of-service")]]
(when (contains? @cf/flags :user-feedback) (when (contains? cf/flags :user-feedback)
[:li.separator {:tab-index (if show [:li.separator {:tab-index (if show
"0" "0"
"-1") "-1")

View file

@ -38,7 +38,7 @@
[:div.modal-left.welcome [:div.modal-left.welcome
[:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] [:img {:src "images/onboarding-welcome.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
[:div.modal-right [:div.modal-right
[:div.release-container [:span.release "Version " (:main @cf/version)]] [:div.release-container [:span.release "Version " (:main cf/version)]]
[:div.right-content [:div.right-content
[:div.modal-title [:div.modal-title
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]] [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.welcome.title")]]
@ -73,7 +73,7 @@
[:div.modal-left.welcome [:div.modal-left.welcome
[:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]] [:img {:src "images/onboarding-people.png" :border "0" :alt (tr "onboarding.welcome.alt")}]]
[:div.modal-right [:div.modal-right
[:div.release-container [:span.release "Version " (:main @cf/version)]] [:div.release-container [:span.release "Version " (:main cf/version)]]
[:div.right-content [:div.right-content
[:div.modal-title [:div.modal-title
[:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]] [:h2 {:data-test "onboarding-welcome"} (tr "onboarding-v2.before-start.title")]]
@ -112,7 +112,7 @@
skip skip
(mf/use-fn (mf/use-fn
#(st/emit! (modal/hide) #(st/emit! (modal/hide)
(if (contains? @cf/flags :newsletter-subscription) (if (contains? cf/flags :newsletter-subscription)
(modal/show {:type :onboarding-newsletter-modal}) (modal/show {:type :onboarding-newsletter-modal})
(modal/show {:type :onboarding-team})) (modal/show {:type :onboarding-team}))
(du/mark-onboarding-as-viewed)))] (du/mark-onboarding-as-viewed)))]

View file

@ -6,6 +6,7 @@
(ns app.main.ui.onboarding.templates (ns app.main.ui.onboarding.templates
(:require (:require
[app.common.data.macros :as dm]
[app.config :as cf] [app.config :as cf]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -21,7 +22,7 @@
(mf/defc template-item (mf/defc template-item
[{:keys [name path image project-id]}] [{:keys [name path image project-id]}]
(let [downloading? (mf/use-state false) (let [downloading? (mf/use-state false)
link (str (assoc @cf/public-uri :path path)) link (dm/str (assoc cf/public-uri :path path))
on-finish-import on-finish-import
(fn [] (fn []

View file

@ -60,7 +60,7 @@
show-release-notes show-release-notes
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
(let [version (:main @cf/version)] (let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event)) (if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding})) (st/emit! (modal/show {:type :onboarding}))
@ -91,7 +91,7 @@
i/tree i/tree
[:span.element-title (tr "labels.settings")]] [:span.element-title (tr "labels.settings")]]
(when (contains? @cf/flags :access-tokens) (when (contains? cf/flags :access-tokens)
[:li {:class (when access-tokens? "current") [:li {:class (when access-tokens? "current")
:on-click go-settings-access-tokens :on-click go-settings-access-tokens
:data-test "settings-access-tokens"} :data-test "settings-access-tokens"}
@ -104,7 +104,7 @@
i/pencil i/pencil
[:span.element-title (tr "labels.release-notes")]] [:span.element-title (tr "labels.release-notes")]]
(when (contains? @cf/flags :user-feedback) (when (contains? cf/flags :user-feedback)
[:li {:class (when feedback? "current") [:li {:class (when feedback? "current")
:on-click go-settings-feedback} :on-click go-settings-feedback}
i/msg-info i/msg-info

View file

@ -16,10 +16,13 @@
(defn- load-fonts! (defn- load-fonts!
[content] [content]
(let [default (:font-id txt/default-text-attrs)] (let [extract-fn (juxt :font-id :font-variant-id)
default (extract-fn txt/default-text-attrs)]
(->> (tree-seq map? :children content) (->> (tree-seq map? :children content)
(into #{default} (keep :font-id)) (into #{default} (keep extract-fn))
(run! fonts/ensure-loaded!)))) (run! (fn [[font-id variant-id]]
(when (some? font-id)
(fonts/ensure-loaded! font-id variant-id)))))))
(mf/defc text-shape (mf/defc text-shape
{::mf/wrap-props false} {::mf/wrap-props false}

View file

@ -145,7 +145,7 @@
(assoc qparams :zoom zoom-type)) (assoc qparams :zoom zoom-type))
href (rt/resolve router :viewer pparams qparams)] href (rt/resolve router :viewer pparams qparams)]
(assoc @cf/public-uri :fragment href)))] (assoc cf/public-uri :fragment href)))]
(reset! link (some-> href str))))) (reset! link (some-> href str)))))
[:div.modal-overlay.transparent.share-modal [:div.modal-overlay.transparent.share-modal

View file

@ -158,7 +158,7 @@
show-release-notes show-release-notes
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(let [version (:main @cf/version)] (let [version (:main cf/version)]
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
(if (and (kbd/alt? event) (kbd/mod? event)) (if (and (kbd/alt? event) (kbd/mod? event))
(st/emit! (modal/show {:type :onboarding})) (st/emit! (modal/show {:type :onboarding}))
@ -186,7 +186,7 @@
[:span (tr "label.shortcuts")] [:span (tr "label.shortcuts")]
[:span.shortcut (sc/get-tooltip :show-shortcuts)]] [:span.shortcut (sc/get-tooltip :show-shortcuts)]]
(when (contains? @cf/flags :user-feedback) (when (contains? cf/flags :user-feedback)
[:* [:*
[:li.feedback {:on-click nav-to-feedback} [:li.feedback {:on-click nav-to-feedback}
[:span (tr "labels.give-feedback")]]])]])) [:span (tr "labels.give-feedback")]]])]]))

View file

@ -10,35 +10,30 @@
[app.main.errors :as err] [app.main.errors :as err]
[app.util.worker :as uw])) [app.util.worker :as uw]))
(defonce instance (atom nil)) (defonce instance nil)
(defn- update-public-uri!
[instance val]
(uw/ask! instance {:cmd :configure
:key :public-uri
:val val}))
(defn init! (defn init!
[] []
(let [worker (uw/init cf/worker-uri err/on-error)] (let [worker (uw/init cf/worker-uri err/on-error)]
(update-public-uri! worker @cf/public-uri) (uw/ask! worker {:cmd :configure
(add-watch cf/public-uri ::worker-public-uri (fn [_ _ _ val] (update-public-uri! worker val))) :key :public-uri
(reset! instance worker))) :val cf/public-uri})
(set! instance worker)))
(defn ask! (defn ask!
([message] ([message]
(when @instance (uw/ask! @instance message))) (when instance (uw/ask! instance message)))
([message transfer] ([message transfer]
(when @instance (uw/ask! @instance 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] ([message transfer]
(when @instance (uw/ask-buffered! @instance 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] ([message transfer]
(when @instance (uw/ask-many! @instance message transfer)))) (when instance (uw/ask-many! instance message transfer))))

View file

@ -0,0 +1,246 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.thumbnail-renderer
"A main entry point for the thumbnail renderer process that is
executed on a separated iframe."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as log]
[app.config :as cf]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.object :as obj]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]))
(log/set-level! :trace)
(declare send-success!)
(declare send-failure!)
(defonce parent-origin
(dm/str cf/public-uri))
(defn- get-document-element
[^js svg]
(.-documentElement svg))
(defn- create-image
[uri]
(rx/create
(fn [subs]
(let [image (js/Image.)]
(obj/set! image "onload" #(do
(rx/push! subs image)
(rx/end! subs)))
(obj/set! image "crossOrigin" "anonymous")
(obj/set! image "onerror" #(rx/error! subs %))
(obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal
:code :abort
:hint "operation aborted")))
(obj/set! image "src" uri)
(fn []
(obj/set! image "src" "")
(obj/set! image "onload" nil)
(obj/set! image "onerror" nil)
(obj/set! image "onabort" nil))))))
(defn- svg-get-size
[svg max]
(let [doc (get-document-element svg)
vbox (dom/get-attribute doc "viewBox")]
(when (string? vbox)
(let [[_ _ width height] (str/split vbox #"\s+")
width (d/parse-integer width 0)
height (d/parse-integer height 0)
ratio (/ width height)]
(if (> width height)
[max (* max (/ 1 ratio))]
[(* max ratio) max])))))
(defn- svg-has-intrinsic-size?
"Returns true if the SVG has an intrinsic size."
[svg]
(let [doc (get-document-element svg)
width (dom/get-attribute doc "width")
height (dom/get-attribute doc "height")]
(d/num? width height)))
(defn- svg-set-intrinsic-size!
"Sets the intrinsic size of an SVG to the given max size."
[^js svg max]
(when-not (svg-has-intrinsic-size? svg)
(let [doc (get-document-element svg)
[w h] (svg-get-size svg max)]
(dom/set-attribute! doc "width" (dm/str w))
(dom/set-attribute! doc "height" (dm/str h))))
svg)
(defn- fetch-as-data-uri
"Fetches a URL as a Data URI."
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get
:mode :cors
:omit-default-headers true})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)))
(defn- svg-update-image!
"Updates an image in an SVG to a Data URI."
[image]
(when-let [href (dom/get-attribute image "href")]
(->> (fetch-as-data-uri href)
(rx/map (fn [url]
(dom/set-attribute! image "href" url)
image)))))
(defn- svg-resolve-images!
"Resolves all images in an SVG to Data URIs."
[svg]
(->> (rx/from (dom/query-all svg "image"))
(rx/mapcat svg-update-image!)
(rx/ignore)))
(defn- svg-add-style!
"Adds a <style> node to an SVG."
[svg styles]
(let [doc (get-document-element svg)
style (dom/create-element svg "http://www.w3.org/2000/svg" "style")]
(dom/append-child! style (dom/create-text svg styles))
(dom/append-child! doc style)))
(defn- svg-resolve-styles!
"Resolves all fonts in an SVG to Data URIs."
[svg styles]
(->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles))
(rx/map second)
(rx/mapcat (fn [url]
(->> (fetch-as-data-uri url)
(rx/map (fn [uri] [url uri])))))
(rx/reduce (fn [styles [url uri]]
(str/replace styles url uri))
styles)
(rx/tap (partial svg-add-style! svg))
(rx/ignore)))
(defn- svg-resolve-all!
"Resolves all images and fonts in an SVG to Data URIs."
[svg styles]
(rx/concat
(svg-resolve-images! svg)
(svg-resolve-styles! svg styles)
(rx/of svg)))
(defn- svg-parse
"Parses an SVG string into an SVG DOM."
[data]
(let [parser (js/DOMParser.)]
(.parseFromString ^js parser data "image/svg+xml")))
(defn- svg-stringify
"Converts an SVG to a string."
[svg]
(let [doc (get-document-element svg)
serializer (js/XMLSerializer.)]
(.serializeToString ^js serializer doc)))
(defn- svg-prepare
"Prepares an SVG for rendering (resolves images to Data URIs and adds intrinsic size)."
[data styles]
(let [svg (svg-parse data)]
(->> (svg-resolve-all! svg styles)
(rx/map #(svg-set-intrinsic-size! % 300))
(rx/map svg-stringify))))
(defn- bitmap->blob
"Converts an ImageBitmap to a Blob."
[bitmap]
(rx/create
(fn [subs]
(let [canvas (dom/create-element "canvas")]
(set! (.-width ^js canvas) (.-width ^js bitmap))
(set! (.-height ^js canvas) (.-height ^js bitmap))
(let [context (.getContext ^js canvas "bitmaprenderer")]
(.transferFromImageBitmap ^js context bitmap)
(.toBlob canvas #(do (rx/push! subs %)
(rx/end! subs))))
(constantly nil)))))
(defn- render
"Renders a thumbnail using it's SVG and returns an ArrayBuffer of the image."
[payload]
(let [data (unchecked-get payload "data")
styles (unchecked-get payload "styles")]
(->> (svg-prepare data styles)
(rx/map #(wapi/create-blob % "image/svg+xml"))
(rx/map wapi/create-uri)
(rx/mapcat (fn [uri]
(->> (create-image uri)
(rx/mapcat wapi/create-image-bitmap)
(rx/tap #(wapi/revoke-uri uri)))))
(rx/mapcat bitmap->blob))))
(defn- on-message
"Handles messages from the main thread."
[event]
(let [evdata (unchecked-get event "data")
evorigin (unchecked-get event "origin")]
(when (str/starts-with? parent-origin evorigin)
(let [id (unchecked-get evdata "id")
payload (unchecked-get evdata "payload")
scope (unchecked-get evdata "scope")]
(when (and (some? payload)
(= scope "penpot/thumbnail-renderer"))
(->> (render payload)
(rx/subs (partial send-success! id)
(partial send-failure! id))))))))
(defn- listen
"Initializes the listener for messages from the main thread."
[]
(.addEventListener js/window "message" on-message))
(defn- send-answer!
"Sends an answer message."
[id type payload]
(let [message #js {:id id
:type type
:scope "penpot/thumbnail-renderer"
:payload payload}]
(when-not (identical? js/window js/parent)
(.postMessage js/parent message parent-origin))))
(defn- send-success!
"Sends a success message."
[id payload]
(send-answer! id "success" payload))
(defn- send-failure!
"Sends a failure message."
[id payload]
(send-answer! id "failure" payload))
(defn- send-ready!
"Sends a ready message."
[]
(send-answer! nil "ready" nil))
(defn ^:export init
[]
(listen)
(send-ready!)
(log/info :hint "initialized"
:public-uri (dm/str cf/public-uri)
:parent-uri (dm/str parent-origin)))

View file

@ -254,7 +254,15 @@
([tag] ([tag]
(.createElement globals/document tag)) (.createElement globals/document tag))
([ns tag] ([ns tag]
(.createElementNS globals/document ns tag))) (.createElementNS globals/document ns tag))
([document ns tag]
(.createElementNS document ns tag)))
(defn create-text
([^js text]
(create-text globals/document text))
([document ^js text]
(.createTextNode document text)))
(defn set-html! (defn set-html!
[^js el html] [^js el html]

View file

@ -51,7 +51,7 @@
(into {} (map vec) (seq (.entries ^js headers)))) (into {} (map vec) (seq (.entries ^js headers))))
(def default-headers (def default-headers
{"x-frontend-version" (:full @cfg/version)}) {"x-frontend-version" (:full cfg/version)})
(defn fetch (defn fetch
[{:keys [method uri query headers body mode omit-default-headers credentials] [{:keys [method uri query headers body mode omit-default-headers credentials]

View file

@ -117,7 +117,7 @@
(let [router (:router state) (let [router (:router state)
path (resolve router rname path-params query-params) path (resolve router rname path-params query-params)
name (or name "_blank") name (or name "_blank")
uri (assoc @cf/public-uri :fragment path)] uri (assoc cf/public-uri :fragment path)]
(dom/open-new-window uri name nil))))) (dom/open-new-window uri name nil)))))
(defn nav-back (defn nav-back

View file

@ -130,6 +130,10 @@
(map #(.item file-list %)) (map #(.item file-list %))
(filter #(str/starts-with? (.-type %) "image/")))))) (filter #(str/starts-with? (.-type %) "image/"))))))
(defn create-image-bitmap
[image]
(js/createImageBitmap image))
(defn request-fullscreen (defn request-fullscreen
[el] [el]
(cond (cond

View file

@ -51,7 +51,7 @@
(defmethod handler :configure (defmethod handler :configure
[{:keys [key val]}] [{:keys [key val]}]
(log/info :hint "configure worker" :key key :val val) (log/info :hint "configure worker" :key key :val (dm/str val))
(case key (case key
:public-uri :public-uri
(reset! cf/public-uri val))) (set! cf/public-uri val)))

View file

@ -18,7 +18,7 @@
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.core :as rx] [beicon.core :as rx]
[debug :refer [debug?]] [okulary.core :as l]
[promesa.core :as p] [promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -45,15 +45,6 @@
:http-status status :http-status status
:http-body body}))) :http-body body})))
(defn- not-found?
[{:keys [type]}]
(= :not-found type))
(defn- body-too-large?
[{:keys [type code]}]
(and (= :validation type)
(= :request-body-too-large code)))
(defn- request-data-for-thumbnail (defn- request-data-for-thumbnail
[file-id revn features] [file-id revn features]
(let [path "api/rpc/command/get-file-data-for-thumbnail" (let [path "api/rpc/command/get-file-data-for-thumbnail"
@ -62,20 +53,7 @@
:strip-frames-with-thumbnails true :strip-frames-with-thumbnails true
:features features} :features features}
request {:method :get request {:method :get
:uri (u/join @cf/public-uri path) :uri (u/join cf/public-uri path)
:credentials "include"
:query params}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
(defn- request-thumbnail
[file-id revn]
(let [path "api/rpc/command/get-file-thumbnail"
params {:file-id file-id
:revn revn}
request {:method :get
:uri (u/join @cf/public-uri path)
:credentials "include" :credentials "include"
:query params}] :query params}]
(->> (http/send! request) (->> (http/send! request)
@ -84,55 +62,23 @@
(defn- render-thumbnail (defn- render-thumbnail
[{:keys [page file-id revn] :as params}] [{:keys [page file-id revn] :as params}]
(binding [fonts/loaded-hints (l/atom #{})]
(let [objects (:objects page) (let [objects (:objects page)
frame (some->> page :thumbnail-frame-id (get objects)) frame (some->> page :thumbnail-frame-id (get objects))
element (if frame element (if frame
(mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true}) (mf/element render/frame-svg #js {:objects objects :frame frame :show-thumbnails? true})
(mf/element render/page-svg #js {:data page :thumbnails? true})) (mf/element render/page-svg #js {:data page :thumbnails? true :render-embed? true}))
data (rds/renderToStaticMarkup element)] data (rds/renderToStaticMarkup element)]
{:data data {:data data
:fonts (into @fonts/loaded (map first) @fonts/loading) :fonts @fonts/loaded-hints
:file-id file-id :file-id file-id
:revn revn})) :revn revn})))
(defn- persist-thumbnail
[{:keys [file-id data revn fonts]}]
(let [path "api/rpc/command/upsert-file-thumbnail"
params {:file-id file-id
:revn revn
:props {:fonts fonts}
:data data}
request {:method :post
:uri (u/join @cf/public-uri path)
:credentials "include"
:body (http/transit-data params)}]
(->> (http/send! request)
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)
(rx/catch body-too-large? (constantly (rx/of nil)))
(rx/map (constantly params)))))
(defmethod impl/handler :thumbnails/generate-for-file (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]}]
{:data data
:fonts (:fonts props)})
(on-cache-miss [_]
(log/debug :hint "request-thumbnail" :file-id file-id :revn revn :cache "miss")
(->> (request-data-for-thumbnail file-id revn features) (->> (request-data-for-thumbnail file-id revn features)
(rx/map render-thumbnail) (rx/map render-thumbnail)))
(rx/mapcat persist-thumbnail)))]
(if (debug? :disable-thumbnail-cache)
(->> (request-data-for-thumbnail file-id revn features)
(rx/map render-thumbnail))
(->> (request-thumbnail file-id revn)
(rx/tap (fn [_]
(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 (defmethod impl/handler :thumbnails/render-offscreen-canvas
[_ ibpm] [_ ibpm]