;; 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.viewer (:require [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages :as cp] [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.constants :as c] [app.main.repo :as rp] [app.main.store :as st] [app.main.data.comments :as dcm] [app.util.avatars :as avatars] [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) ;; --- General Specs (s/def ::id ::us/uuid) (s/def ::name ::us/string) (s/def ::project (s/keys ::req-un [::id ::name])) (s/def ::file (s/keys :req-un [::id ::name])) (s/def ::page ::cp/page) (s/def ::bundle (s/keys :req-un [::project ::file ::page])) ;; --- Local State Initialization (def ^:private default-local-state {:zoom 1 :interactions-mode :hide :interactions-show? false :comments-mode :all :comments-show :unresolved :selected #{} :collapsed #{} :hover nil}) (declare fetch-comment-threads) (declare fetch-bundle) (declare bundle-fetched) (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::index ::us/integer) (s/def ::token (s/nilable ::us/string)) (s/def ::section ::us/string) (s/def ::initialize-params (s/keys :req-un [::page-id ::file-id] :opt-in [::token])) (defn initialize [{:keys [page-id file-id token] :as params}] (us/assert ::initialize-params params) (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] (-> state (assoc :current-file-id file-id) (assoc :current-page-id page-id) (update :viewer-local (fn [lstate] (if (nil? lstate) default-local-state lstate))))) ptk/WatchEvent (watch [_ state stream] (rx/of (fetch-bundle params) (fetch-comment-threads params))))) ;; --- Data Fetching (s/def ::fetch-bundle-params (s/keys :req-un [::page-id ::file-id] :opt-in [::token])) (defn fetch-bundle [{:keys [page-id file-id token] :as params}] (us/assert ::fetch-bundle-params params) (ptk/reify ::fetch-file ptk/WatchEvent (watch [_ state stream] (let [params (cond-> {:page-id page-id :file-id file-id} (string? token) (assoc :token token))] (->> (rp/query :viewer-bundle params) (rx/map bundle-fetched)))))) (defn- extract-frames [objects] (let [root (get objects uuid/zero)] (into [] (comp (map #(get objects %)) (filter #(= :frame (:type %)))) (reverse (:shapes root))))) (defn bundle-fetched [{:keys [project file page share-token token libraries users] :as bundle}] (us/verify ::bundle bundle) (ptk/reify ::file-fetched ptk/UpdateEvent (update [_ state] (let [objects (:objects page) frames (extract-frames objects)] (assoc state :viewer-libraries (d/index-by :id libraries) :viewer-data {:project project :objects objects :users (d/index-by :id users) :file file :page page :frames frames :token token :share-token share-token}))))) (defn fetch-comment-threads [{:keys [file-id page-id] :as params}] (letfn [(fetched [data state] (->> data (filter #(= page-id (:page-id %))) (d/index-by :id) (assoc state :comment-threads))) (on-error [{:keys [type] :as err}] (if (= :authentication type) (rx/empty) (rx/throw err)))] (ptk/reify ::fetch-comment-threads ptk/WatchEvent (watch [_ state stream] (->> (rp/query :comment-threads {:file-id file-id}) (rx/map #(partial fetched %)) (rx/catch on-error)))))) (defn refresh-comment-thread [{:keys [id file-id] :as thread}] (letfn [(fetched [thread state] (assoc-in state [:comment-threads id] thread))] (ptk/reify ::refresh-comment-thread ptk/WatchEvent (watch [_ state stream] (->> (rp/query :comment-thread {:file-id file-id :id id}) (rx/map #(partial fetched %))))))) (defn fetch-comments [{:keys [thread-id]}] (us/assert ::us/uuid thread-id) (letfn [(fetched [comments state] (update state :comments assoc thread-id (d/index-by :id comments)))] (ptk/reify ::retrieve-comments ptk/WatchEvent (watch [_ state stream] (->> (rp/query :comments {:thread-id thread-id}) (rx/map #(partial fetched %))))))) (defn create-share-link [] (ptk/reify ::create-share-link ptk/WatchEvent (watch [_ state stream] (let [file-id (:current-file-id state) page-id (:current-page-id state)] (->> (rp/mutation! :create-file-share-token {:file-id file-id :page-id page-id}) (rx/map (fn [{:keys [token]}] #(assoc-in % [:viewer-data :token] token)))))))) (defn delete-share-link [] (ptk/reify ::delete-share-link ptk/WatchEvent (watch [_ state stream] (let [file-id (:current-file-id state) page-id (:current-page-id state) token (get-in state [:viewer-data :token]) params {:file-id file-id :page-id page-id :token token}] (->> (rp/mutation :delete-file-share-token params) (rx/map (fn [_] #(update % :viewer-data dissoc :token)))))))) ;; --- Zoom Management (def increase-zoom (ptk/reify ::increase-zoom ptk/UpdateEvent (update [_ state] (let [increase #(nth c/zoom-levels (+ (d/index-of c/zoom-levels %) 1) (last c/zoom-levels))] (update-in state [:viewer-local :zoom] (fnil increase 1)))))) (def decrease-zoom (ptk/reify ::decrease-zoom ptk/UpdateEvent (update [_ state] (let [decrease #(nth c/zoom-levels (- (d/index-of c/zoom-levels %) 1) (first c/zoom-levels))] (update-in state [:viewer-local :zoom] (fnil decrease 1)))))) (def reset-zoom (ptk/reify ::reset-zoom ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :zoom] 1)))) (def zoom-to-50 (ptk/reify ::zoom-to-50 ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :zoom] 0.5)))) (def zoom-to-200 (ptk/reify ::zoom-to-200 ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :zoom] 2)))) ;; --- Local State Management (def toggle-thumbnails-panel (ptk/reify ::toggle-thumbnails-panel ptk/UpdateEvent (update [_ state] (update-in state [:viewer-local :show-thumbnails] not)))) (def select-prev-frame (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state stream] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) pparams (:path-params route) index (:index qparams)] (when (pos? index) (rx/of (dcm/close-thread) (rt/nav screen pparams (assoc qparams :index (dec index))))))))) (def select-next-frame (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state stream] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) pparams (:path-params route) index (:index qparams) total (count (get-in state [:viewer-data :frames]))] (when (< index (dec total)) (rx/of (dcm/close-thread) (rt/nav screen pparams (assoc qparams :index (inc index))))))))) (s/def ::interactions-mode #{:hide :show :show-on-click}) (defn set-interactions-mode [mode] (us/verify ::interactions-mode mode) (ptk/reify ::set-interactions-mode ptk/UpdateEvent (update [_ state] (-> state (assoc-in [:viewer-local :interactions-mode] mode) (assoc-in [:viewer-local :interactions-show?] (case mode :hide false :show true :show-on-click false)))))) (declare flash-done) (def flash-interactions (ptk/reify ::flash-interactions ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :interactions-show?] true)) ptk/WatchEvent (watch [_ state stream] (let [stopper (rx/filter (ptk/type? ::flash-interactions) stream)] (->> (rx/of flash-done) (rx/delay 500) (rx/take-until stopper)))))) (def flash-done (ptk/reify ::flash-done ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :interactions-show?] false)))) ;; --- Navigation (defn go-to-frame-by-index [index] (ptk/reify ::go-to-frame ptk/WatchEvent (watch [_ state stream] (let [route (:route state) screen (-> route :data :name keyword) qparams (:query-params route) pparams (:path-params route)] (rx/of (rt/nav screen pparams (assoc qparams :index index))))))) (defn go-to-frame [frame-id] (us/verify ::us/uuid frame-id) (ptk/reify ::go-to-frame ptk/WatchEvent (watch [_ state stream] (let [frames (get-in state [:viewer-data :frames]) index (d/index-of-pred frames #(= (:id %) frame-id))] (when index (rx/of (go-to-frame-by-index index))))))) (defn go-to-section [section] (ptk/reify ::go-to-section ptk/WatchEvent (watch [_ state stream] (let [route (:route state) screen (-> route :data :name keyword) pparams (:path-params route) qparams (:query-params route)] (rx/of (if (= :handoff section) (rt/nav :handoff pparams qparams) (rt/nav :viewer pparams (assoc qparams :section section)))))))) (defn set-current-frame [frame-id] (ptk/reify ::current-frame ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-data :current-frame-id] frame-id)))) (defn deselect-all [] (ptk/reify ::deselect-all ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :selected] #{})))) (defn select-shape ([id] (ptk/reify ::select-shape ptk/UpdateEvent (update [_ state] (-> state (assoc-in [:viewer-local :selected] #{id})))))) (defn toggle-selection [id] (ptk/reify ::toggle-selection ptk/UpdateEvent (update [_ state] (let [selected (get-in state [:viewer-local :selected])] (cond-> state (not (selected id)) (update-in [:viewer-local :selected] conj id) (selected id) (update-in [:viewer-local :selected] disj id)))))) (defn shift-select-to [id] (ptk/reify ::shift-select-to ptk/UpdateEvent (update [_ state] (let [objects (get-in state [:viewer-data :objects]) selection (-> state (get-in [:viewer-local :selected] #{}) (conj id))] (-> state (assoc-in [:viewer-local :selected] (cp/expand-region-selection objects selection))))))) (defn select-all [] (ptk/reify ::select-all ptk/UpdateEvent (update [_ state] (let [objects (get-in state [:viewer-data :objects]) frame-id (get-in state [:viewer-data :current-frame-id]) selection (->> objects (filter #(= (:frame-id (second %)) frame-id)) (map first) (into #{frame-id}))] (-> state (assoc-in [:viewer-local :selected] selection)))))) (defn toggle-collapse [id] (ptk/reify ::toggle-collapse ptk/UpdateEvent (update [_ state] (let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)] (update-in state [:viewer-local :collapsed] (if toggled? disj conj) id))))) (defn hover-shape [id hover?] (ptk/reify ::hover-shape ptk/UpdateEvent (update [_ state] (assoc-in state [:viewer-local :hover] (when hover? id)))))