Merge pull request #5056 from penpot/niwinz-refactor-recent-colors

♻️ Refactor recent colors and local storage abstraction
This commit is contained in:
Alejandro 2024-09-05 09:07:26 +02:00 committed by GitHub
commit ea8febdb7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 161 additions and 106 deletions

View file

@ -190,10 +190,9 @@
[:type [:= :del-color]] [:type [:= :del-color]]
[:id ::sm/uuid]]] [:id ::sm/uuid]]]
;; DEPRECATED: remove before 2.3
[:add-recent-color [:add-recent-color
[:map {:title "AddRecentColorChange"} [:map {:title "AddRecentColorChange"}]]
[:type [:= :add-recent-color]]
[:color ::ctc/recent-color]]]
[:add-media [:add-media
[:map {:title "AddMediaChange"} [:map {:title "AddMediaChange"}
@ -656,18 +655,10 @@
[data {:keys [id]}] [data {:keys [id]}]
(ctcl/delete-color data id)) (ctcl/delete-color data id))
;; DEPRECATED: remove before 2.3
(defmethod process-change :add-recent-color (defmethod process-change :add-recent-color
[data {:keys [color]}] [data _]
;; Moves the color to the top of the list and then truncates up to 15 data)
(update
data
:recent-colors
(fn [rc]
(let [rc (->> rc (d/removev (partial ctc/eq-recent-color? color)))
rc (-> rc (conj color))]
(cond-> rc
(> (count rc) 15)
(subvec 1))))))
;; -- Media ;; -- Media

View file

@ -607,13 +607,6 @@
(reduce resize-parent changes all-parents))) (reduce resize-parent changes all-parents)))
;; Library changes ;; Library changes
(defn add-recent-color
[changes color]
(-> changes
(update :redo-changes conj {:type :add-recent-color :color color})
(apply-changes-local)))
(defn add-color (defn add-color
[changes color] [changes color]
(-> changes (-> changes

View file

@ -107,17 +107,16 @@
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
(sm/register! ::rgb-color type:rgb-color) (sm/register! ::rgb-color type:rgb-color)
(sm/register! ::color schema:color) (sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient) (sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color) (sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color) (sm/register! ::recent-color schema:recent-color)
(def check-color! (def valid-color?
(sm/check-fn schema:color)) (sm/lazy-validator schema:color))
(def check-recent-color! (def valid-recent-color?
(sm/check-fn schema:recent-color)) (sm/lazy-validator schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; HELPERS
@ -392,13 +391,22 @@
(process-shape-colors shape sync-color))) (process-shape-colors shape sync-color)))
(defn eq-recent-color? (defn- eq-recent-color?
[c1 c2] [c1 c2]
(or (= c1 c2) (or (= c1 c2)
(and (some? (:color c1)) (and (some? (:color c1))
(some? (:color c2)) (some? (:color c2))
(= (:color c1) (:color c2))))) (= (:color c1) (:color c2)))))
(defn add-recent-color
"Moves the color to the top of the list and then truncates up to 15"
[state file-id color]
(update state file-id (fn [colors]
(let [colors (d/removev (partial eq-recent-color? color) colors)
colors (conj colors color)]
(cond-> colors
(> (count colors) 15)
(subvec 1))))))
(defn stroke->color-att (defn stroke->color-att
[stroke file-id shared-libs] [stroke file-id shared-libs]

View file

@ -21,7 +21,7 @@
[app.main.repo :as rp] [app.main.repo :as rp]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :refer [storage]] [app.util.storage :as s]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
@ -51,14 +51,14 @@
(defn get-current-team-id (defn get-current-team-id
[profile] [profile]
(let [team-id (::current-team-id @storage)] (let [team-id (::current-team-id @s/storage)]
(or team-id (:default-team-id profile)))) (or team-id (:default-team-id profile))))
(defn set-current-team! (defn set-current-team!
[team-id] [team-id]
(if (nil? team-id) (if (nil? team-id)
(swap! storage dissoc ::current-team-id) (swap! s/storage dissoc ::current-team-id)
(swap! storage assoc ::current-team-id team-id))) (swap! s/storage assoc ::current-team-id team-id)))
;; --- EVENT: fetch-teams ;; --- EVENT: fetch-teams
@ -78,9 +78,9 @@
;; if not, dissoc it from storage. ;; if not, dissoc it from storage.
(let [ids (into #{} (map :id) teams)] (let [ids (into #{} (map :id) teams)]
(when-let [ctid (::current-team-id @storage)] (when-let [ctid (::current-team-id @s/storage)]
(when-not (contains? ids ctid) (when-not (contains? ids ctid)
(swap! storage dissoc ::current-team-id))))))) (swap! s/storage dissoc ::current-team-id)))))))
(defn fetch-teams (defn fetch-teams
[] []
@ -131,10 +131,10 @@
(effect [_ state _] (effect [_ state _]
(let [profile (:profile state) (let [profile (:profile state)
email (:email profile) email (:email profile)
previous-profile (:profile @storage) previous-profile (:profile @s/storage)
previous-email (:email previous-profile)] previous-email (:email previous-profile)]
(when profile (when profile
(swap! storage assoc :profile profile) (swap! s/storage assoc :profile profile)
(i18n/set-locale! (:lang profile)) (i18n/set-locale! (:lang profile))
(when (not= previous-email email) (when (not= previous-email email)
(set-current-team! nil))))))) (set-current-team! nil)))))))
@ -320,7 +320,7 @@
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ _] (effect [_ _ _]
;; We prefer to keek some stuff in the storage like the current-team-id and the profile ;; We prefer to keek some stuff in the storage like the current-team-id and the profile
(set-current-team! nil))))) (swap! s/storage (constantly {}))))))
(defn logout (defn logout
([] (logout {})) ([] (logout {}))

View file

@ -79,6 +79,7 @@
[app.util.http :as http] [app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :refer [storage]]
[app.util.timers :as tm] [app.util.timers :as tm]
[app.util.webapi :as wapi] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@ -335,6 +336,7 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc state (assoc state
:recent-colors (:recent-colors @storage)
:workspace-ready? false :workspace-ready? false
:current-file-id file-id :current-file-id file-id
:current-project-id project-id :current-project-id project-id

View file

@ -48,6 +48,7 @@
[app.util.color :as uc] [app.util.color :as uc]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :as s]
[app.util.time :as dt] [app.util.time :as dt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -132,16 +133,21 @@
(defn add-recent-color (defn add-recent-color
[color] [color]
(dm/assert! (dm/assert!
"expected valid recent color map" "expected valid recent color map"
(ctc/check-recent-color! color)) (ctc/valid-recent-color? color))
(ptk/reify ::add-recent-color (ptk/reify ::add-recent-color
ptk/WatchEvent ptk/UpdateEvent
(watch [it _ _] (update [_ state]
(let [changes (-> (pcb/empty-changes it) (let [file-id (:current-file-id state)]
(pcb/add-recent-color color))] (update state :recent-colors ctc/add-recent-color file-id color)))
(rx/of (dch/commit-changes changes))))))
ptk/EffectEvent
(effect [_ state _]
(let [recent-colors (:recent-colors state)]
(swap! s/storage assoc :recent-colors recent-colors)))))
(def clear-color-for-rename (def clear-color-for-rename
(ptk/reify ::clear-color-for-rename (ptk/reify ::clear-color-for-rename
@ -168,8 +174,11 @@
(dm/assert! (dm/assert!
"expected valid parameters" "expected valid parameters"
(and (ctc/check-color! color) (ctc/valid-color? color))
(uuid? file-id)))
(dm/assert!
"expected file-id"
(uuid? file-id))
(ptk/reify ::update-color (ptk/reify ::update-color
ptk/WatchEvent ptk/WatchEvent

View file

@ -236,9 +236,10 @@
=)) =))
(def workspace-recent-colors (def workspace-recent-colors
(l/derived (fn [data] (l/derived (fn [state]
(get data :recent-colors [])) (when-let [file-id (:current-file-id state)]
workspace-data)) (dm/get-in state [:recent-colors file-id])))
st/state))
(def workspace-recent-fonts (def workspace-recent-fonts
(l/derived (fn [data] (l/derived (fn [data]

View file

@ -294,19 +294,21 @@
`key` for new values." `key` for new values."
[key default] [key default]
(let [id (mf/use-id) (let [id (mf/use-id)
state (mf/use-state (get @storage key default)) state* (mf/use-state #(get @storage key default))
state (deref state*)
stream (mf/with-memo [id] stream (mf/with-memo [id]
(->> mbc/stream (->> mbc/stream
(rx/filter #(not= (:id %) id)) (rx/filter #(not= (:id %) id))
(rx/filter #(= (:type %) key)) (rx/filter #(= (:type %) key))
(rx/map deref)))] (rx/map deref)))]
(mf/with-effect [@state key id] (mf/with-effect [state key id]
(mbc/emit! id key @state) (mbc/emit! id key state)
(swap! storage assoc key @state)) (swap! storage assoc key state))
(use-stream stream (partial reset! state)) (use-stream stream (partial reset! state*))
state))
state*))
(defonce ^:private intersection-subject (rx/subject)) (defonce ^:private intersection-subject (rx/subject))
(defonce ^:private intersection-observer (defonce ^:private intersection-observer

View file

@ -6,6 +6,7 @@
(ns app.main.ui.hooks.resize (ns app.main.ui.hooks.resize
(:require (:require
[app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.logging :as log] [app.common.logging :as log]
@ -20,6 +21,15 @@
(def last-resize-type nil) (def last-resize-type nil)
(defn- get-initial-state
[initial file-id key]
(let [saved (dm/get-in @storage [::state file-id key])]
(d/nilv saved initial)))
(defn- update-persistent-state
[data file-id key size]
(update-in data [::state file-id] assoc key size))
(defn set-resize-type! [type] (defn set-resize-type! [type]
(set! last-resize-type type)) (set! last-resize-type type))
@ -28,26 +38,28 @@
(use-resize-hook key initial min-val max-val axis negate? resize-type nil)) (use-resize-hook key initial min-val max-val axis negate? resize-type nil))
([key initial min-val max-val axis negate? resize-type on-change-size] ([key initial min-val max-val axis negate? resize-type on-change-size]
(let [current-file-id (mf/use-ctx ctx/current-file-id) (let [file-id (mf/use-ctx ctx/current-file-id)
size-state (mf/use-state (or (get-in @storage [::saved-resize current-file-id key]) initial))
parent-ref (mf/use-ref nil)
current-size* (mf/use-state #(get-initial-state initial file-id key))
current-size (deref current-size*)
parent-ref (mf/use-ref nil)
dragging-ref (mf/use-ref false) dragging-ref (mf/use-ref false)
start-size-ref (mf/use-ref nil) start-size-ref (mf/use-ref nil)
start-ref (mf/use-ref nil) start-ref (mf/use-ref nil)
on-pointer-down on-pointer-down
(mf/use-callback (mf/use-fn
(mf/deps @size-state) (mf/deps current-size)
(fn [event] (fn [event]
(dom/capture-pointer event) (dom/capture-pointer event)
(mf/set-ref-val! start-size-ref @size-state) (mf/set-ref-val! start-size-ref current-size)
(mf/set-ref-val! dragging-ref true) (mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-ref (dom/get-client-position event)) (mf/set-ref-val! start-ref (dom/get-client-position event))
(set! last-resize-type resize-type))) (set! last-resize-type resize-type)))
on-lost-pointer-capture on-lost-pointer-capture
(mf/use-callback (mf/use-fn
(fn [event] (fn [event]
(dom/release-pointer event) (dom/release-pointer event)
(mf/set-ref-val! start-size-ref nil) (mf/set-ref-val! start-size-ref nil)
@ -56,8 +68,8 @@
(set! last-resize-type nil))) (set! last-resize-type nil)))
on-pointer-move on-pointer-move
(mf/use-callback (mf/use-fn
(mf/deps min-val max-val negate?) (mf/deps min-val max-val negate? file-id key)
(fn [event] (fn [event]
(when (mf/ref-val dragging-ref) (when (mf/ref-val dragging-ref)
(let [start (mf/ref-val start-ref) (let [start (mf/ref-val start-ref)
@ -65,31 +77,30 @@
delta (-> (gpt/to-vec start pos) delta (-> (gpt/to-vec start pos)
(cond-> negate? gpt/negate) (cond-> negate? gpt/negate)
(get axis)) (get axis))
start-size (mf/ref-val start-size-ref) start-size (mf/ref-val start-size-ref)
new-size (-> (+ start-size delta) (max min-val) (min max-val))] new-size (-> (+ start-size delta) (max min-val) (min max-val))]
(reset! size-state new-size) (reset! current-size* new-size)
(swap! storage assoc-in [::saved-resize current-file-id key] new-size) (swap! storage update-persistent-state file-id key new-size)))))
(when on-change-size (on-change-size new-size))))))
set-size set-size
(mf/use-callback (mf/use-fn
(mf/deps on-change-size) (mf/deps on-change-size file-id key)
(fn [new-size] (fn [new-size]
(let [new-size (mth/clamp new-size min-val max-val)] (let [new-size (mth/clamp new-size min-val max-val)]
(reset! size-state new-size) (reset! current-size* new-size)
(swap! storage assoc-in [::saved-resize current-file-id key] new-size) (swap! storage update-persistent-state file-id key new-size))))]
(when on-change-size (on-change-size new-size)))))]
(mf/use-effect (mf/with-effect [on-change-size current-size]
(fn [] (when on-change-size
(when on-change-size (on-change-size @size-state)))) (on-change-size current-size)))
{:on-pointer-down on-pointer-down {:on-pointer-down on-pointer-down
:on-lost-pointer-capture on-lost-pointer-capture :on-lost-pointer-capture on-lost-pointer-capture
:on-pointer-move on-pointer-move :on-pointer-move on-pointer-move
:parent-ref parent-ref :parent-ref parent-ref
:set-size set-size :set-size set-size
:size @size-state}))) :size current-size})))
(defn use-resize-observer (defn use-resize-observer
[callback] [callback]

View file

@ -6,42 +6,80 @@
(ns app.util.storage (ns app.util.storage
(:require (:require
["lodash/debounce" :as ldebounce]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.transit :as t] [app.common.transit :as t]
[app.util.globals :as g] [app.util.globals :as g]
[app.util.timers :as tm])) [cuerdas.core :as str]))
(defn- persist ;; Using ex/ignoring because can receive a DOMException like this when
[storage prev curr] ;; importing the code as a library: Failed to read the 'localStorage'
(run! (fn [key] ;; property from 'Window': Storage is disabled inside 'data:' URLs.
(let [prev* (get prev key) (defonce ^:private local-storage
curr* (get curr key)] (ex/ignoring (unchecked-get g/global "localStorage")))
(when (not= curr* prev*)
(tm/schedule-on-idle
#(if (some? curr*)
(.setItem ^js storage (t/encode-str key) (t/encode-str curr*))
(.removeItem ^js storage (t/encode-str key)))))))
(into #{} (concat (keys curr) (defn- encode-key
(keys prev))))) [k]
(assert (keyword? k) "key must be keyword")
(let [kns (namespace k)
kn (name k)]
(str "penpot:" kns "/" kn)))
(defn- decode-key
[k]
(when (str/starts-with? k "penpot:")
(let [k (subs k 7)]
(if (str/starts-with? k "/")
(keyword (subs k 1))
(let [[kns kn] (str/split k "/" 2)]
(keyword kns kn))))))
(defn- lookup-by-index
[result index]
(try
(let [key (.key ^js local-storage index)
key' (decode-key key)]
(if key'
(let [val (.getItem ^js local-storage key)]
(assoc! result key' (t/decode-str val)))
result))
(catch :default _
result)))
(defn- load (defn- load
[storage] []
(when storage (when (some? local-storage)
(let [len (.-length ^js storage)] (let [length (.-length ^js local-storage)]
(reduce (fn [res index] (loop [index 0
(let [key (.key ^js storage index) result (transient {})]
val (.getItem ^js storage key)] (if (< index length)
(recur (inc index)
(lookup-by-index result index))
(persistent! result))))))
(defonce ^:private latest-state (load))
(defn- on-change*
[curr-state]
(let [prev-state latest-state]
(try (try
(assoc res (t/decode-str key) (t/decode-str val)) (run! (fn [key]
(catch :default _e (let [prev-val (get prev-state key)
res)))) curr-val (get curr-state key)]
{} (when-not (identical? curr-val prev-val)
(range len))))) (if (some? curr-val)
(.setItem ^js local-storage (encode-key key) (t/encode-str curr-val))
(.removeItem ^js local-storage (encode-key key))))))
(into #{} (concat (keys curr-state)
(keys prev-state))))
(finally
(set! latest-state curr-state)))))
;; Using ex/ignoring because can receive a DOMException like this when importing the code as a library: (defonce on-change
;; Failed to read the 'localStorage' property from 'Window': Storage is disabled inside 'data:' URLs. (ldebounce on-change* 2000 #js {:leading false :trailing true}))
(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage")))))
(add-watch storage :persistence #(persist js/localStorage %3 %4))
(defonce storage (atom latest-state))
(add-watch storage :persistence
(fn [_ _ _ curr-state]
(on-change curr-state)))