;; 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/. ;; ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; ;; Copyright (c) 2020 UXBOX Labs SL (ns app.main.data.workspace.persistence (:require [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.media :as cm] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.data.dashboard :as dd] [app.main.data.media :as di] [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :as i18n :refer [tr]] [app.util.object :as obj] [app.util.router :as rt] [app.util.time :as dt] [app.util.transit :as t] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) (declare persist-changes) (declare shapes-changes-persisted) ;; --- Persistence (defn initialize-file-persistence [file-id] (letfn [(enable-reload-stoper [] (obj/set! js/window "onbeforeunload" (constantly false))) (disable-reload-stoper [] (obj/set! js/window "onbeforeunload" nil))] (ptk/reify ::initialize-persistence ptk/WatchEvent (watch [_ state stream] (let [stoper (rx/filter #(= ::finalize %) stream) notifier (->> stream (rx/filter (ptk/type? ::dwc/commit-changes)) (rx/debounce 2000) (rx/merge stoper))] (rx/merge (->> stream (rx/filter (ptk/type? ::dwc/commit-changes)) (rx/map deref) (rx/tap enable-reload-stoper) (rx/buffer-until notifier) (rx/map vec) (rx/filter (complement empty?)) (rx/map #(persist-changes file-id %)) (rx/take-until (rx/delay 100 stoper))) (->> stream (rx/filter (ptk/type? ::changes-persisted)) (rx/tap disable-reload-stoper) (rx/ignore) (rx/take-until stoper)))))))) (defn persist-changes [file-id changes] (ptk/reify ::persist-changes ptk/WatchEvent (watch [_ state stream] (let [sid (:session-id state) file (:workspace-file state)] (when (= (:id file) file-id) (let [changes (into [] (mapcat identity) changes) params {:id (:id file) :revn (:revn file) :session-id sid :changes changes}] (->> (rp/mutation :update-file params) (rx/map (fn [lagged] (if (= #{sid} (into #{} (map :session-id) lagged)) (map #(assoc % :changes []) lagged) lagged))) (rx/mapcat seq) (rx/map #(shapes-changes-persisted file-id %))))))))) (s/def ::shapes-changes-persisted (s/keys :req-un [::revn ::cp/changes])) (defn shapes-changes-persisted [file-id {:keys [revn changes] :as params}] (us/verify ::us/uuid file-id) (us/verify ::shapes-changes-persisted params) (ptk/reify ::changes-persisted ptk/UpdateEvent (update [_ state] (let [sid (:session-id state) file (:workspace-file state)] (if (= file-id (:id file)) (let [state (update-in state [:workspace-file :revn] #(max % revn))] (-> state (update :workspace-data cp/process-changes changes) (update-in [:workspace-file :data] cp/process-changes changes))) state))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching & Uploading ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; --- Specs (s/def ::id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::name string?) (s/def ::type keyword?) (s/def ::file-id ::us/uuid) (s/def ::created-at ::us/inst) (s/def ::modified-at ::us/inst) (s/def ::version ::us/integer) (s/def ::revn ::us/integer) (s/def ::ordering ::us/integer) (s/def ::metadata (s/nilable ::cp/metadata)) (s/def ::data ::cp/data) (s/def ::file ::dd/file) (s/def ::project ::dd/project) (s/def ::page (s/keys :req-un [::id ::name ::file-id ::revn ::created-at ::modified-at ::ordering ::data])) (declare fetch-libraries-content) (declare bundle-fetched) (defn- fetch-bundle [project-id file-id] (ptk/reify ::fetch-bundle ptk/WatchEvent (watch [_ state stream] (->> (rx/zip (rp/query :file {:id file-id}) (rp/query :file-users {:id file-id}) (rp/query :project-by-id {:project-id project-id}) (rp/query :file-libraries {:file-id file-id})) (rx/first) (rx/map (fn [bundle] (apply bundle-fetched bundle))) (rx/catch (fn [{:keys [type code] :as error}] (cond (= :not-found type) (rx/of (rt/nav' :not-found)) (and (= :authentication type) (= :unauthorized code)) (rx/of (rt/nav' :not-authorized)) :else (throw error)))))))) (defn- bundle-fetched [file users project libraries] (ptk/reify ::bundle-fetched IDeref (-deref [_] {:file file :users users :project project :libraries libraries}) ptk/UpdateEvent (update [_ state] (assoc state :workspace-undo {} :workspace-project project :workspace-file file :workspace-data (:data file) :workspace-users (d/index-by :id users) :workspace-libraries (d/index-by :id libraries))))) ;; --- Set File shared (defn set-file-shared [id is-shared] {:pre [(uuid? id) (boolean? is-shared)]} (ptk/reify ::set-file-shared ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-file :is-shared] is-shared)) ptk/WatchEvent (watch [_ state stream] (let [params {:id id :is-shared is-shared}] (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) ;; --- Fetch Shared Files (declare shared-files-fetched) (defn fetch-shared-files [{:keys [team-id] :as params}] (us/assert ::us/uuid team-id) (ptk/reify ::fetch-shared-files ptk/WatchEvent (watch [_ state stream] (->> (rp/query :shared-files params) (rx/map shared-files-fetched))))) (defn shared-files-fetched [files] (us/verify (s/every ::file) files) (ptk/reify ::shared-files-fetched ptk/UpdateEvent (update [_ state] (let [state (dissoc state :files)] (assoc state :workspace-shared-files files))))) ;; --- Link and unlink Files (declare file-linked) (defn link-file-to-library [file-id library-id] (ptk/reify ::link-file-to-library ptk/WatchEvent (watch [_ state stream] (let [params {:file-id file-id :library-id library-id}] (->> (->> (rp/mutation :link-file-to-library params) (rx/mapcat #(rx/zip (rp/query :file-library {:file-id library-id}) (rp/query :media-objects {:file-id library-id :is-local false}) (rp/query :colors {:file-id library-id})))) (rx/map file-linked)))))) (defn file-linked [[library media-objects colors]] (ptk/reify ::file-linked ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-libraries (:id library)] (assoc library :media-objects media-objects :colors colors))))) (declare file-unlinked) (defn unlink-file-from-library [file-id library-id] (ptk/reify ::unlink-file-from-library ptk/WatchEvent (watch [_ state stream] (let [params {:file-id file-id :library-id library-id}] (->> (rp/mutation :unlink-file-from-library params) (rx/map #(file-unlinked file-id library-id))))))) (defn file-unlinked [file-id library-id] (ptk/reify ::file-unlinked ptk/UpdateEvent (update [_ state] (d/dissoc-in state [:workspace-libraries library-id])))) ;; --- Fetch Pages (declare page-fetched) (defn fetch-page [page-id] (us/verify ::us/uuid page-id) (ptk/reify ::fetch-pages ptk/WatchEvent (watch [_ state s] (->> (rp/query :page {:id page-id}) (rx/map page-fetched))))) (defn page-fetched [{:keys [id] :as page}] (us/verify ::page page) (ptk/reify ::page-fetched IDeref (-deref [_] page) ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-pages id] page)))) ;; --- Upload local media objects (s/def ::local? ::us/boolean) (s/def ::uri ::us/string) (s/def ::upload-media-objects-params (s/keys :req-un [::file-id ::local?] :opt-un [::uri ::di/js-files])) (defn upload-media-objects [{:keys [file-id local? js-files uri] :as params}] (us/assert ::upload-media-objects-params params) (ptk/reify ::upload-media-objects ptk/WatchEvent (watch [_ state stream] (let [{:keys [on-success on-error] :or {on-success identity}} (meta params) is-library (not= file-id (:id (:workspace-file state))) prepare-js-file (fn [js-file] {:name (.-name js-file) :file-id file-id :content js-file :is-local local?}) prepare-uri (fn [uri] {:file-id file-id :is-local local? :url uri})] (rx/concat (rx/of (dm/show {:content (tr "media.loading") :type :info :timeout nil})) (->> (if (string? uri) (->> (rx/of uri) (rx/map prepare-uri) (rx/mapcat #(rp/mutation! :add-media-object-from-url %))) (->> (rx/from js-files) (rx/map di/validate-file) (rx/map prepare-js-file) (rx/mapcat #(rp/mutation! :upload-media-object %)))) (rx/do on-success) (rx/catch (fn [error] (cond (= (:code error) :media-type-not-allowed) (rx/of (dm/error (tr "errors.media-type-not-allowed"))) (= (:code error) :media-type-mismatch) (rx/of (dm/error (tr "errors.media-type-mismatch"))) (fn? on-error) (do (on-error error) (rx/empty)) :else (rx/throw error)))) (rx/finalize (fn [] (st/emit! dm/hide))))))))) ;; --- Delete media object (defn delete-media-object [file-id id] (ptk/reify ::delete-media-object ptk/WatchEvent (watch [_ state stream] (let [params {:id id}] (rp/mutation :delete-media-object params))))) ;; --- Helpers (defn purge-page "Remove page and all related stuff from the state." [state id] (-> state (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) (update :workspace-pages dissoc id)))