From 64a6ba194905465a459f186b1be8384f23c62f35 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 18 Nov 2020 17:36:14 +0100 Subject: [PATCH] :tada: Add comments to viewer. --- backend/src/app/services/queries/files.clj | 2 + backend/src/app/services/queries/viewer.clj | 3 + common/app/common/spec.cljc | 1 + frontend/resources/locales.json | 19 +- .../styles/main/partials/dropdown.scss | 31 +- .../styles/main/partials/viewer-header.scss | 82 +-- .../styles/main/partials/viewer.scss | 5 +- .../main/partials/workspace-comments.scss | 33 +- frontend/src/app/main.cljs | 17 +- frontend/src/app/main/data/comments.cljs | 300 +++++++++++ frontend/src/app/main/data/viewer.cljs | 144 ++++-- frontend/src/app/main/data/workspace.cljs | 2 + .../src/app/main/data/workspace/comments.cljs | 254 +--------- .../app/main/data/workspace/persistence.cljs | 10 +- frontend/src/app/main/refs.cljs | 4 + frontend/src/app/main/repo.cljs | 5 +- frontend/src/app/main/ui.cljs | 177 ++++--- frontend/src/app/main/ui/comments.cljs | 340 +++++++++++++ .../app/main/ui/components/fullscreen.cljs | 57 +++ frontend/src/app/main/ui/context.cljs | 1 + .../src/app/main/ui/{viewer => }/handoff.cljs | 108 ++-- .../ui/{viewer => }/handoff/attributes.cljs | 18 +- .../{viewer => }/handoff/attributes/blur.cljs | 2 +- .../handoff/attributes/common.cljs | 2 +- .../{viewer => }/handoff/attributes/fill.cljs | 4 +- .../handoff/attributes/image.cljs | 2 +- .../handoff/attributes/layout.cljs | 4 +- .../handoff/attributes/shadow.cljs | 4 +- .../handoff/attributes/stroke.cljs | 4 +- .../{viewer => }/handoff/attributes/text.cljs | 4 +- .../main/ui/{viewer => }/handoff/code.cljs | 2 +- .../main/ui/{viewer => }/handoff/exports.cljs | 8 +- .../ui/{viewer => }/handoff/left_sidebar.cljs | 14 +- .../main/ui/{viewer => }/handoff/render.cljs | 4 +- .../{viewer => }/handoff/right_sidebar.cljs | 6 +- .../handoff/selection_feedback.cljs | 2 +- frontend/src/app/main/ui/hooks.cljs | 17 - frontend/src/app/main/ui/viewer.cljs | 261 ++++++++-- frontend/src/app/main/ui/viewer/header.cljs | 215 +++++--- .../src/app/main/ui/viewer/thumbnails.cljs | 3 +- .../src/app/main/ui/workspace/comments.cljs | 466 +++--------------- frontend/src/app/util/avatars.cljs | 6 + frontend/src/app/util/dom.cljs | 32 +- frontend/src/app/util/object.cljs | 4 + frontend/src/app/util/webapi.cljs | 24 +- 45 files changed, 1629 insertions(+), 1074 deletions(-) create mode 100644 frontend/src/app/main/data/comments.cljs create mode 100644 frontend/src/app/main/ui/comments.cljs create mode 100644 frontend/src/app/main/ui/components/fullscreen.cljs rename frontend/src/app/main/ui/{viewer => }/handoff.cljs (55%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes.cljs (74%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/blur.cljs (96%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/common.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/fill.cljs (94%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/image.cljs (97%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/layout.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/shadow.cljs (95%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/stroke.cljs (95%) rename frontend/src/app/main/ui/{viewer => }/handoff/attributes/text.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/code.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/exports.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/left_sidebar.cljs (96%) rename frontend/src/app/main/ui/{viewer => }/handoff/render.cljs (98%) rename frontend/src/app/main/ui/{viewer => }/handoff/right_sidebar.cljs (94%) rename frontend/src/app/main/ui/{viewer => }/handoff/selection_feedback.cljs (98%) diff --git a/backend/src/app/services/queries/files.clj b/backend/src/app/services/queries/files.clj index 8fa7106ec..c83d4a80e 100644 --- a/backend/src/app/services/queries/files.clj +++ b/backend/src/app/services/queries/files.clj @@ -203,6 +203,8 @@ (defn retrieve-file-users [conn id] (->> (db/exec! conn [sql:file-users id id]) + ;; TODO: seems like the frontend is no longer uses :photo-uri, + ;; so this can be removed probably. (mapv #(media/resolve-media-uris % [:photo :photo-uri])))) (s/def ::file-users diff --git a/backend/src/app/services/queries/viewer.clj b/backend/src/app/services/queries/viewer.clj index 172654e8b..0174f5b12 100644 --- a/backend/src/app/services/queries/viewer.clj +++ b/backend/src/app/services/queries/viewer.clj @@ -50,8 +50,11 @@ file (merge (dissoc file :data) (select-keys (:data file) [:colors :media :typographies])) libs (files/retrieve-file-libraries conn false file-id) + users (files/retrieve-file-users conn file-id) + bundle {:file file :page page + :users users :project project :libraries libs}] (if (string? token) diff --git a/common/app/common/spec.cljc b/common/app/common/spec.cljc index b9ab80199..4223c93f3 100644 --- a/common/app/common/spec.cljc +++ b/common/app/common/spec.cljc @@ -119,6 +119,7 @@ (s/def ::url string?) (s/def ::fn fn?) (s/def ::point gpt/point?) +(s/def ::id ::uuid) ;; --- Macros diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 844251b9c..0c1951a89 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -1608,28 +1608,19 @@ "modals.delete-comment-thread.accept" : { "used-in" : [ "src/app/main/ui/workspace/comments.cljs:236" ], "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null + "en" : "Delete conversation" } }, "modals.delete-comment-thread.message" : { "used-in" : [ "src/app/main/ui/workspace/comments.cljs:235" ], "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null + "en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted." } }, "modals.delete-comment-thread.title" : { "used-in" : [ "src/app/main/ui/workspace/comments.cljs:234" ], "translations" : { - "en" : null, - "fr" : null, - "ru" : null, - "es" : null + "en" : "Delete conversation" } }, "modals.delete-file-confirm.accept" : { @@ -1881,6 +1872,10 @@ "es" : "No se encuentra el tablero." } }, + "labels.show-all-comments": "Show all comments", + "labels.show-your-comments": "Show only yours comments", + "labels.hide-resolved-comments": "Hide resolved comments", + "viewer.header.dont-show-interactions" : { "used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ], "translations" : { diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss index 496cc78fd..0802e04d2 100644 --- a/frontend/resources/styles/main/partials/dropdown.scss +++ b/frontend/resources/styles/main/partials/dropdown.scss @@ -11,7 +11,7 @@ border-color: $color-gray-10; } - li { + > li { display: flex; align-items: center; color: $color-gray-60; @@ -20,6 +20,12 @@ height: 40px; padding: 5px 16px; + svg { + fill: $color-gray-20; + height: 12px; + width: 12px; + } + &.title { font-weight: 600; cursor: default; @@ -29,4 +35,27 @@ background-color: $color-primary-lighter; } } + + + &.with-check { + > li { + padding: 5px 10px; + } + + > li:not(.selected) { + svg { display: none; } + } + + svg { + fill: $color-gray-50; + } + + .icon { + display: flex; + align-items: center; + width: 25px; + height: 25px; + margin-right: 7px; + } + } } diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 4648e1c09..1edb9a0ac 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -42,28 +42,31 @@ } } - .header-icon { - align-items: center; - cursor: pointer; - display: flex; - justify-content: center; - - a { - height: 16px; - width: 16px; - - svg { - fill: $color-gray-30; - height: 16px; - width: 16px; - } + .view-options { + .icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; &:hover { - svg { + > svg { fill: $color-primary; } } } + + svg { + fill: $color-gray-30; + height: 16px; + width: 16px; + } + + .dropdown { + top: 40px; + left: 0px; + width: 260px; + } } .sitemap-zone { @@ -92,12 +95,10 @@ } } - .dropdown-button { - svg { - fill: $color-white; - height: 10px; - width: 10px; - } + .show-thumbnails-button svg { + fill: $color-white; + height: 10px; + width: 10px; } .page-name { @@ -243,42 +244,9 @@ } - .custom-select-dropdown { - position: absolute; - left: 0; - z-index: 12; - max-height: 31rem; - min-width: 7rem; - overflow-y: auto; - - background-color: $color-white; - border-radius: $br-small; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); - - li { - color: $color-gray-60; - cursor: pointer; - font-size: $fs14; - display: flex; - padding: $small $medium $small 25px; - - &.selected { - background-image: url(/images/icons/tick.svg); - background-repeat: no-repeat; - background-position: 5% 48%; - background-size: 10px; - font-weight: bold; - } - - &:hover { - background-color: $color-primary-lighter; - } - } - } - .zoom-dropdown { - left : 116px; - top: 45px; + left: 180px; + top: 40px; } .users-zone { diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index 5ac5b7611..346db1c83 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -7,19 +7,18 @@ } .viewer-preview { - height: 100vh; + height: calc(100vh - 40px); grid-row: 1 / span 2; grid-column: 1 / span 1; - overflow: scroll; + overflow: auto; display: flex; justify-content: center; align-items: center; flex-flow: wrap; - .empty-state { justify-content: center; align-items: center; diff --git a/frontend/resources/styles/main/partials/workspace-comments.scss b/frontend/resources/styles/main/partials/workspace-comments.scss index 6c5174774..1f425ae71 100644 --- a/frontend/resources/styles/main/partials/workspace-comments.scss +++ b/frontend/resources/styles/main/partials/workspace-comments.scss @@ -1,14 +1,29 @@ -.workspace-comments { +.viewer-comments { width: 100%; height: 100%; - grid-column: 1/span 2; - grid-row: 1/span 2; z-index: 1000; - pointer-events: none; - overflow: hidden; + position: absolute; + top: 0px; + left: 0px; +} - .threads { - position: relative; +.viewer-comments, .workspace-comments { + + .comments-layer { + + width: 100%; + height: 100%; + grid-column: 1/span 2; + grid-row: 1/span 2; + z-index: 1000; + pointer-events: none; + overflow: hidden; + + .threads { + position: absolute; + top: 0px; + left: 0px; + } } .thread-bubble { @@ -104,8 +119,6 @@ } } - - .comment-container { position: relative; } @@ -132,7 +145,7 @@ font-size: $fs13; @include text-ellipsis; - width: 110px; + width: 150px; } .timeago { diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 70053ffc4..787c60de3 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -10,6 +10,7 @@ (ns app.main (:require [app.common.uuid :as uuid] + [app.common.spec :as us] [app.main.data.auth :refer [logout]] [app.main.data.users :as udu] [app.main.store :as st] @@ -35,12 +36,26 @@ (declare reinit) +(s/def ::any any?) + +(defn match-path + [router path] + (when-let [match (rt/match router path)] + (if-let [conform (get-in match [:data :conform])] + (let [spath (get conform :path-params ::any) + squery (get conform :query-params ::any)] + (-> (dissoc match :params) + (assoc :path-params (us/conform spath (get match :path-params)) + :query-params (us/conform squery (get match :query-params))))) + match))) + (defn on-navigate [router path] - (let [match (rt/match router path) + (let [match (match-path router path) profile (:profile storage) authed? (and (not (nil? profile)) (not= (:id profile) uuid/zero))] + (cond (and (or (= path "") (nil? match)) diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs new file mode 100644 index 000000000..79b5995a9 --- /dev/null +++ b/frontend/src/app/main/data/comments.cljs @@ -0,0 +1,300 @@ +;; 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.comments + (:require + [cuerdas.core :as str] + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as geom] + [app.common.math :as mth] + [app.common.pages :as cp] + [app.common.pages-helpers :as cph] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cfg] + [app.main.constants :as c] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.streams :as ms] + [app.main.worker :as uw] + [app.util.router :as rt] + [app.util.timers :as ts] + [app.util.transit :as t] + [app.util.webapi :as wapi] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [clojure.set :as set] + [potok.core :as ptk])) + +(s/def ::content ::us/string) +(s/def ::count-comments ::us/integer) +(s/def ::count-unread-comments ::us/integer) +(s/def ::created-at ::us/inst) +(s/def ::file-id ::us/uuid) +(s/def ::modified-at ::us/inst) +(s/def ::owner-id ::us/uuid) +(s/def ::page-id ::us/uuid) +(s/def ::participants (s/every ::us/uuid :kind set?)) +(s/def ::position ::us/point) +(s/def ::seqn ::us/integer) +(s/def ::thread-id ::us/uuid) + +(s/def ::comment-thread + (s/keys :req-un [::us/id + ::page-id + ::file-id + ::seqn + ::content + ::participants + ::count-unread-comments + ::count-comments + ::created-at + ::modified-at + ::owner-id + ::position])) + +(s/def ::comment + (s/keys :req-un [::us/id + ::thread-id + ::owner-id + ::created-at + ::modified-at + ::content])) + +(declare create-draft-thread) +(declare retrieve-comment-threads) +(declare refresh-comment-thread) + +(s/def ::create-thread-params + (s/keys :req-un [::page-id ::file-id ::position ::content])) + +(defn create-thread + [params] + (us/assert ::create-thread-params params) + (letfn [(created [{:keys [id comment] :as thread} state] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update :comments-local assoc :open id) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))] + + (ptk/reify ::create-thread + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :create-comment-thread params) + (rx/map #(partial created %))))))) + +(defn update-comment-thread-status + [{:keys [id] :as thread}] + (us/assert ::comment-thread thread) + (ptk/reify ::update-comment-thread-status + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:comment-threads id] assoc :count-unread-comments 0)) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :update-comment-thread-status {:id id}) + (rx/ignore))))) + + +(defn update-comment-thread + [{:keys [id is-resolved] :as thread}] + (us/assert ::comment-thread thread) + (ptk/reify ::update-comment-thread + + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:comment-threads id] assoc :is-resolved is-resolved)) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved}) + (rx/ignore))))) + + +(defn add-comment + [thread content] + (us/assert ::comment-thread thread) + (us/assert ::us/string content) + (letfn [(created [comment state] + (update-in state [:comments (:id thread)] assoc (:id comment) comment))] + (ptk/reify ::create-comment + ptk/WatchEvent + (watch [_ state stream] + (rx/concat + (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content}) + (rx/map #(partial created %))) + (rx/of (refresh-comment-thread thread))))))) + +(defn update-comment + [{:keys [id content thread-id] :as comment}] + (us/assert ::comment comment) + (ptk/reify :update-comment + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:comments thread-id id] assoc :content content)) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :update-comment {:id id :content content}) + (rx/ignore))))) + +(defn delete-comment-thread + [{:keys [id] :as thread}] + (us/assert ::comment-thread thread) + (ptk/reify :delete-comment-thread + ptk/UpdateEvent + (update [_ state] + (-> state + (update :comments dissoc id) + (update :comment-threads dissoc id))) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :delete-comment-thread {:id id}) + (rx/ignore))))) + +(defn delete-comment + [{:keys [id thread-id] :as comment}] + (us/assert ::comment comment) + (ptk/reify :delete-comment + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:comments thread-id] dissoc id)) + + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/mutation :delete-comment {:id id}) + (rx/ignore))))) + +(defn refresh-comment-thread + [{:keys [id file-id] :as thread}] + (us/assert ::comment-thread 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 retrieve-comment-threads + [file-id] + (us/assert ::us/uuid file-id) + (letfn [(fetched [data state] + (assoc state :comment-threads (d/index-by :id data)))] + (ptk/reify ::retrieve-comment-threads + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :comment-threads {:file-id file-id}) + (rx/map #(partial fetched %))))))) + +(defn retrieve-comments + [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 %))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Local State +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn open-thread + [{:keys [id] :as thread}] + (us/assert ::comment-thread thread) + (ptk/reify ::open-thread + ptk/UpdateEvent + (update [_ state] + (-> state + (update :comments-local assoc :open id) + (update :workspace-drawing dissoc :comment))))) + +(defn close-thread + [] + (ptk/reify ::close-thread + ptk/UpdateEvent + (update [_ state] + (-> state + (update :comments-local dissoc :open :draft) + (update :workspace-drawing dissoc :comment))))) + +(defn update-filters + [{:keys [mode show] :as params}] + (ptk/reify ::update-filters + ptk/UpdateEvent + (update [_ state] + (update state :comments-local + (fn [local] + (cond-> local + (some? mode) + (assoc :mode mode) + + (some? show) + (assoc :show show))))))) + +(s/def ::create-draft-params + (s/keys :req-un [::page-id ::file-id ::position])) + +(defn create-draft + [params] + (us/assert ::create-draft-params params) + (ptk/reify ::create-draft + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-drawing assoc :comment params) + (update :comments-local assoc :draft params))))) + +(defn update-draft-thread + [data] + (ptk/reify ::update-draft-thread + ptk/UpdateEvent + (update [_ state] + (-> state + (d/update-in-when [:workspace-drawing :comment] merge data) + (d/update-in-when [:comments-local :draft] merge data))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn group-threads-by-page + [threads] + (letfn [(group-by-page [result thread] + (let [current (first result)] + (if (= (:page-id current) (:page-id thread)) + (cons (update current :items conj thread) + (rest result)) + (cons {:page-id (:page-id thread) :items [thread]} + result))))] + (reverse + (reduce group-by-page nil threads)))) + +(defn apply-filters + [cstate profile threads] + (let [{:keys [show mode open]} cstate] + (cond->> threads + (= :pending show) + (filter (fn [item] + (or (not (:is-resolved item)) + (= (:id item) open)))) + + (= :yours mode) + (filter #(contains? (:participants %) (:id profile)))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index b40917be6..3aa7058f5 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -23,7 +23,7 @@ [app.common.uuid :as uuid] [app.common.pages-helpers :as cph])) -;; --- Specs +;; --- General Specs (s/def ::id ::us/uuid) (s/def ::name ::us/string) @@ -32,40 +32,63 @@ (s/def ::file (s/keys :req-un [::id ::name])) (s/def ::page ::cp/page) -(s/def ::interactions-mode #{:hide :show :show-on-click}) - (s/def ::bundle (s/keys :req-un [::project ::file ::page])) -;; --- Initialization +;; --- 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] :as params}] + [{:keys [page-id file-id token] :as params}] + (us/assert ::initialize-params params) (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] - (assoc state :viewer-local {:zoom 1 - :page-id page-id - :file-id file-id - :interactions-mode :hide - :show-interactions? false - - :selected #{} - :collapsed #{} - :hover nil})) + (update state :viewer-local + (fn [lstate] + (if (nil? lstate) + default-local-state + lstate)))) ptk/WatchEvent (watch [_ state stream] - (rx/of (fetch-bundle params))))) + (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]}] + [{:keys [page-id file-id token] :as params}] + (us/assert ::fetch-bundle-params params) (ptk/reify ::fetch-file ptk/WatchEvent (watch [_ state stream] @@ -76,6 +99,40 @@ (rx/first) (rx/map bundle-fetched)))))) +(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)))] + (ptk/reify ::fetch-comment-threads + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :comment-threads {:file-id file-id}) + (rx/map #(partial fetched %))))))) + +(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- extract-frames [objects] (let [root (get objects uuid/zero)] @@ -86,7 +143,7 @@ (vec)))) (defn bundle-fetched - [{:keys [project file page share-token token libraries] :as bundle}] + [{:keys [project file page share-token token libraries users] :as bundle}] (us/verify ::bundle bundle) (ptk/reify ::file-fetched ptk/UpdateEvent @@ -94,9 +151,10 @@ (let [objects (:objects page) frames (extract-frames objects)] (-> state - (assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries)) + (assoc :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 @@ -176,11 +234,11 @@ (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state stream] - (let [route (:route state) - screen (-> route :data :name keyword) - qparams (get-in route [:params :query]) - pparams (get-in route [:params :path]) - index (d/parse-integer (:index qparams))] + (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 (rt/nav screen pparams (assoc qparams :index (dec index))))))))) @@ -188,15 +246,17 @@ (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state stream] - (let [route (:route state) - screen (-> route :data :name keyword) - qparams (get-in route [:params :query]) - pparams (get-in route [:params :path]) - index (d/parse-integer (:index qparams)) + (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 (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) @@ -205,7 +265,7 @@ (update [_ state] (-> state (assoc-in [:viewer-local :interactions-mode] mode) - (assoc-in [:viewer-local :show-interactions?] (case mode + (assoc-in [:viewer-local :interactions-show?] (case mode :hide false :show true :show-on-click false)))))) @@ -216,7 +276,7 @@ (ptk/reify ::flash-interactions ptk/UpdateEvent (update [_ state] - (assoc-in state [:viewer-local :show-interactions?] true)) + (assoc-in state [:viewer-local :interactions-show?] true)) ptk/WatchEvent (watch [_ state stream] @@ -229,26 +289,30 @@ (ptk/reify ::flash-done ptk/UpdateEvent (update [_ state] - (assoc-in state [:viewer-local :show-interactions?] false)))) + (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 [page-id (get-in state [:viewer-local :page-id]) - file-id (get-in state [:viewer-local :file-id]) - frames (get-in state [:viewer-data :frames]) - token (get-in state [:viewer-data :token]) + (let [frames (get-in state [:viewer-data :frames]) index (d/index-of-pred frames #(= (:id %) frame-id))] - (rx/of (rt/nav :viewer - {:page-id page-id - :file-id file-id} - {:token token - :index index})))))) + (rx/of (go-to-frame-by-index index)))))) (defn set-current-frame [frame-id] (ptk/reify ::current-frame diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 24cbc6b98..6daa481ec 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -144,6 +144,8 @@ ptk/UpdateEvent (update [_ state] (assoc state + :current-file-id file-id + :current-project-id project-id :workspace-presence {})) ptk/WatchEvent diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index b355be375..ac1d16c05 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -9,59 +9,34 @@ (ns app.main.data.workspace.comments (:require - [cuerdas.core :as str] [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.shapes :as geom] [app.common.math :as mth] - [app.common.pages :as cp] - [app.common.pages-helpers :as cph] [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cfg] [app.main.constants :as c] [app.main.data.workspace.common :as dwc] - [app.main.repo :as rp] + [app.main.data.comments :as dcm] [app.main.store :as st] [app.main.streams :as ms] - [app.main.worker :as uw] - [app.util.router :as rt] - [app.util.timers :as ts] - [app.util.transit :as t] - [app.util.webapi :as wapi] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [potok.core :as ptk])) -(s/def ::comment-thread any?) -(s/def ::comment any?) - -(declare create-draft-thread) -(declare retrieve-comment-threads) -(declare refresh-comment-thread) (declare handle-interrupt) (declare handle-comment-layer-click) (defn initialize-comments [file-id] (us/assert ::us/uuid file-id) - (ptk/reify ::start-commenting - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local assoc :commenting true)) - + (ptk/reify ::initialize-comments ptk/WatchEvent (watch [_ state stream] (let [stoper (rx/filter #(= ::finalize %) stream)] (rx/merge - (rx/of (retrieve-comment-threads file-id)) + (rx/of (dcm/retrieve-comment-threads file-id)) (->> stream (rx/filter ms/mouse-click?) (rx/switch-map #(rx/take 1 ms/mouse-position)) - (rx/mapcat #(rx/take 1 ms/mouse-position)) (rx/map handle-comment-layer-click) (rx/take-until stoper)) (->> stream @@ -72,19 +47,13 @@ (defn- handle-interrupt [] (ptk/reify ::handle-interrupt - ptk/UpdateEvent - (update [_ state] - (let [local (:workspace-comments state) - drawing (:workspace-drawing state)] + ptk/WatchEvent + (watch [_ state stream] + (let [local (:comments-local state)] (cond - (:comment drawing) - (update state :workspace-drawing dissoc :comment) - - (:open local) - (update state :workspace-comments dissoc :open) - - :else - (dissoc state :workspace-drawing)))))) + (:draft local) (rx/of (dcm/close-thread)) + (:open local) (rx/of (dcm/close-thread)) + :else (rx/of #(dissoc % :workspace-drawing))))))) ;; Event responsible of the what should be executed when user clicked ;; on the comments layer. An option can be create a new draft thread, @@ -93,215 +62,32 @@ (defn- handle-comment-layer-click [position] (ptk/reify ::handle-comment-layer-click - ptk/UpdateEvent - (update [_ state] - (let [local (:workspace-comments state)] - (if (:open local) - (update state :workspace-comments dissoc :open) - (update state :workspace-drawing assoc - :comment {:position position :content ""})))))) - -(defn create-thread - [data] - (letfn [(created [{:keys [id comment] :as thread} state] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update :workspace-comments assoc :open id) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment)))] - - (ptk/reify ::create-thread - ptk/WatchEvent - (watch [_ state stream] - (let [file-id (get-in state [:workspace-file :id]) - page-id (:current-page-id state) - params (assoc data - :page-id page-id - :file-id file-id)] - (->> (rp/mutation :create-comment-thread params) - (rx/map #(partial created %)))))))) - -(defn update-comment-thread-status - [{:keys [id] :as thread}] - (us/assert ::comment-thread thread) - (ptk/reify ::update-comment-thread-status - ptk/UpdateEvent - (update [_ state] - (d/update-in-when state [:comment-threads id] assoc :count-unread-comments 0)) - ptk/WatchEvent (watch [_ state stream] - (->> (rp/mutation :update-comment-thread-status {:id id}) - (rx/ignore))))) - - -(defn update-comment-thread - [{:keys [id is-resolved] :as thread}] - (us/assert ::comment-thread thread) - (ptk/reify ::update-comment-thread - - ptk/UpdateEvent - (update [_ state] - (d/update-in-when state [:comment-threads id] assoc :is-resolved is-resolved)) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved}) - (rx/ignore))))) - - -(defn add-comment - [thread content] - (us/assert ::comment-thread thread) - (us/assert ::us/string content) - (letfn [(created [comment state] - (update-in state [:comments (:id thread)] assoc (:id comment) comment))] - (ptk/reify ::create-comment - ptk/WatchEvent - (watch [_ state stream] - (rx/concat - (->> (rp/mutation :add-comment {:thread-id (:id thread) :content content}) - (rx/map #(partial created %))) - (rx/of (refresh-comment-thread thread))))))) - -(defn update-comment - [{:keys [id content thread-id] :as comment}] - (us/assert ::comment comment) - (ptk/reify :update-comment - ptk/UpdateEvent - (update [_ state] - (d/update-in-when state [:comments thread-id id] assoc :content content)) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/mutation :update-comment {:id id :content content}) - (rx/ignore))))) - -(defn delete-comment-thread - [{:keys [id] :as thread}] - (us/assert ::comment-thread thread) - (ptk/reify :delete-comment-thread - ptk/UpdateEvent - (update [_ state] - (-> state - (update :comments dissoc id) - (update :comment-threads dissoc id))) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/mutation :delete-comment-thread {:id id}) - (rx/ignore))))) - -(defn delete-comment - [{:keys [id thread-id] :as comment}] - (us/assert ::comment comment) - (ptk/reify :delete-comment - ptk/UpdateEvent - (update [_ state] - (d/update-in-when state [:comments thread-id] dissoc id)) - - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/mutation :delete-comment {:id id}) - (rx/ignore))))) - -(defn refresh-comment-thread - [{:keys [id file-id] :as thread}] - (us/assert ::comment-thread 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 retrieve-comment-threads - [file-id] - (us/assert ::us/uuid file-id) - (letfn [(fetched [data state] - (assoc state :comment-threads (d/index-by :id data)))] - (ptk/reify ::retrieve-comment-threads - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :comment-threads {:file-id file-id}) - (rx/map #(partial fetched %))))))) - -(defn retrieve-comments - [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 %))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Workspace (local) events -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn open-thread - [{:keys [id] :as thread}] - (us/assert ::comment-thread thread) - (ptk/reify ::open-thread - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-comments assoc :open id) - (update :workspace-drawing dissoc :comment))))) - -(defn close-thread - [] - (ptk/reify ::open-thread - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-comments dissoc :open) - (update :workspace-drawing dissoc :comment))))) - -(defn update-draft-thread - [data] - (ptk/reify ::update-draft-thread - ptk/UpdateEvent - (update [_ state] - (update state :workspace-drawing assoc :comment data)))) - -(defn update-filters - [{:keys [main resolved]}] - (ptk/reify ::update-filters - ptk/UpdateEvent - (update [_ state] - (update state :workspace-comments - (fn [local] - (cond-> local - (some? main) - (assoc :filter main) - - (some? resolved) - (assoc :filter-resolved resolved))))))) - + (let [local (:comments-local state)] + (if (some? (:open local)) + (rx/of (dcm/close-thread)) + (let [page-id (:current-page-id state) + file-id (:current-file-id state) + params {:position position + :page-id page-id + :file-id file-id}] + (rx/of (dcm/create-draft params)))))))) (defn center-to-comment-thread [{:keys [id position] :as thread}] - (us/assert ::comment-thread thread) + (us/assert ::dcm/comment-thread thread) (ptk/reify :center-to-comment-thread ptk/UpdateEvent (update [_ state] (update state :workspace-local (fn [{:keys [vbox vport zoom] :as local}] - ;; (prn "position=" position) - ;; (prn "vbox=" vbox) - ;; (prn "vport=" vport) (let [pw (/ 50 zoom) ph (/ 200 zoom) nw (mth/round (- (/ (:width vbox) 2) pw)) nh (mth/round (- (/ (:height vbox) 2) ph)) nx (- (:x position) nw) ny (- (:y position) nh)] - (update local :vbox assoc :x nx :y ny)))) - - ))) + (update local :vbox assoc :x nx :y ny))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index a35c0ae67..ec3827223 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -37,8 +37,6 @@ ;; --- Persistence - - (defn initialize-file-persistence [file-id] (ptk/reify ::initialize-persistence @@ -225,12 +223,6 @@ :else (throw error)))))))) -(defn assoc-profile-avatar - [{:keys [photo fullname] :as profile}] - (cond-> profile - (or (nil? photo) (empty? photo)) - (assoc :photo (avatars/generate {:name fullname})))) - (defn- bundle-fetched [file users project libraries] (ptk/reify ::bundle-fetched @@ -243,7 +235,7 @@ ptk/UpdateEvent (update [_ state] - (let [users (map assoc-profile-avatar users)] + (let [users (map avatars/assoc-profile-avatar users)] (assoc state :workspace-undo {} :workspace-project project diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index fd3a2c2cd..37d5b7200 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -207,3 +207,7 @@ (def viewer-local (l/derived :viewer-local st/state)) + +(def comments-local + (l/derived :comments-local st/state)) + diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 823d1d373..9221f9ad4 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -31,11 +31,14 @@ (rx/throw {:type :authorization :code :not-authorized}) + (= (:status response) 404) + (rx/throw (:body response)) + (= 0 (:status response)) (rx/throw {:type :offline}) :else - (rx/throw {:type :internal-error + (rx/throw {:type :server-error :status (:status response) :body (:body response)}))) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index e81e7801c..fa0ae92d3 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -13,6 +13,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.uuid :as uuid] + [app.common.spec :as us] [app.main.data.auth :refer [logout]] [app.main.data.messages :as dm] [app.main.refs :as refs] @@ -20,6 +21,7 @@ [app.main.ui.auth :refer [auth]] [app.main.ui.auth.verify-token :refer [verify-token]] [app.main.ui.cursors :as c] + [app.main.ui.context :as ctx] [app.main.ui.dashboard :refer [dashboard]] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] @@ -27,7 +29,7 @@ [app.main.ui.settings :as settings] [app.main.ui.static :refer [not-found-page not-authorized-page]] [app.main.ui.viewer :refer [viewer-page]] - [app.main.ui.viewer.handoff :refer [handoff]] + [app.main.ui.handoff :refer [handoff]] [app.main.ui.workspace :as workspace] [app.util.i18n :as i18n :refer [tr t]] [app.util.timers :as ts] @@ -39,6 +41,19 @@ ;; --- Routes +(s/def ::page-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::viewer-path-params + (s/keys :req-un [::file-id ::page-id])) + +(s/def ::section ::us/keyword) +(s/def ::index ::us/integer) +(s/def ::token (s/nilable ::us/string)) + +(s/def ::viewer-query-params + (s/keys :req-un [::index] + :opt-un [::token ::section])) + (def routes [["/auth" ["/login" :auth-login] @@ -53,8 +68,17 @@ ["/password" :settings-password] ["/options" :settings-options]] - ["/view/:file-id/:page-id" :viewer] - ["/handoff/:file-id/:page-id" :handoff] + ["/view/:file-id/:page-id" + {:name :viewer + :conform + {:path-params ::viewer-path-params + :query-params ::viewer-query-params}}] + + ["/handoff/:file-id/:page-id" + {:name :handoff + :conform {:path-params ::viewer-path-params + :query-params ::viewer-query-params}}] + ["/not-found" :not-found] ["/not-authorized" :not-authorized] @@ -86,84 +110,88 @@ (mf/defc app {::mf/wrap [#(mf/catch % {:fallback app-error})]} [{:keys [route] :as props}] - (case (get-in route [:data :name]) - (:auth-login - :auth-register - :auth-goodbye - :auth-recovery-request - :auth-recovery) - [:& auth {:route route}] - :auth-verify-token - [:& verify-token {:route route}] + [:& (mf/provider ctx/current-route) {:value route} + (case (get-in route [:data :name]) + (:auth-login + :auth-register + :auth-goodbye + :auth-recovery-request + :auth-recovery) + [:& auth {:route route}] - (:settings-profile - :settings-password - :settings-options) - [:& settings/settings {:route route}] + :auth-verify-token + [:& verify-token {:route route}] - :debug-icons-preview - (when *assert* - [:div.debug-preview - [:h1 "Cursors"] - [:& c/debug-preview] - [:h1 "Icons"] - [:& i/debug-icons-preview] - ]) + (:settings-profile + :settings-password + :settings-options) + [:& settings/settings {:route route}] - (:dashboard-search - :dashboard-projects - :dashboard-files - :dashboard-libraries - :dashboard-team-members - :dashboard-team-settings) - [:& dashboard {:route route}] + :debug-icons-preview + (when *assert* + [:div.debug-preview + [:h1 "Cursors"] + [:& c/debug-preview] + [:h1 "Icons"] + [:& i/debug-icons-preview] + ]) - :viewer - (let [index (d/parse-integer (get-in route [:params :query :index])) - token (get-in route [:params :query :token]) - file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :path :page-id]))] - [:& viewer-page {:page-id page-id - :file-id file-id - :index index - :token token}]) + (:dashboard-search + :dashboard-projects + :dashboard-files + :dashboard-libraries + :dashboard-team-members + :dashboard-team-settings) + [:& dashboard {:route route}] - :handoff - (let [index (d/parse-integer (get-in route [:params :query :index])) - file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :path :page-id]))] - [:& handoff {:page-id page-id - :file-id file-id - :index index}]) + :viewer + (let [index (get-in route [:query-params :index]) + token (get-in route [:query-params :token]) + section (get-in route [:query-params :section] :interactions) + file-id (get-in route [:path-params :file-id]) + page-id (get-in route [:path-params :page-id])] + [:& viewer-page {:page-id page-id + :file-id file-id + :section section + :index index + :token token}]) - :render-object - (do - (let [file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :path :page-id])) - object-id (uuid (get-in route [:params :path :object-id]))] - [:& render/render-object {:file-id file-id - :page-id page-id - :object-id object-id}])) + :handoff + (let [file-id (get-in route [:path-params :file-id]) + page-id (get-in route [:path-params :page-id]) + index (get-in route [:query-params :index])] + [:& handoff {:page-id page-id + :file-id file-id + :index index}]) - :workspace - (let [project-id (uuid (get-in route [:params :path :project-id])) - file-id (uuid (get-in route [:params :path :file-id])) - page-id (uuid (get-in route [:params :query :page-id])) - layout-name (get-in route [:params :query :layout])] - [:& workspace/workspace {:project-id project-id - :file-id file-id - :page-id page-id - :layout-name (keyword layout-name) - :key file-id}]) + :render-object + (do + (let [file-id (uuid (get-in route [:params :path :file-id])) + page-id (uuid (get-in route [:params :path :page-id])) + object-id (uuid (get-in route [:params :path :object-id]))] + [:& render/render-object {:file-id file-id + :page-id page-id + :object-id object-id}])) - :not-authorized - [:& not-authorized-page] + :workspace + (let [project-id (uuid (get-in route [:params :path :project-id])) + file-id (uuid (get-in route [:params :path :file-id])) + page-id (uuid (get-in route [:params :query :page-id])) + layout-name (get-in route [:params :query :layout])] + [:& workspace/workspace {:project-id project-id + :file-id file-id + :page-id page-id + :layout-name (keyword layout-name) + :key file-id}]) - :not-found - [:& not-found-page] + :not-authorized + [:& not-authorized-page] - nil)) + :not-found + [:& not-found-page] + + nil)]) (mf/defc app-wrapper [] @@ -229,7 +257,7 @@ :type :error :timeout 5000})))))) -(defmethod ptk/handle-error :internal-error +(defmethod ptk/handle-error :server-error [{:keys [status] :as error}] (cond (= status 429) @@ -243,6 +271,13 @@ (st/emitf (dm/show {:content "Unable to connect to backend, wait a little bit and refresh." :type :error}))))) + +(defmethod ptk/handle-error :not-found + [{:keys [status] :as error}] + (ts/schedule + (st/emitf (dm/show {:content "Resource not found." + :type :warning})))) + (defonce uncaught-error-handler (letfn [(on-error [event] (ptk/handle-error (unchecked-get event "error")) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs new file mode 100644 index 000000000..3cf96c363 --- /dev/null +++ b/frontend/src/app/main/ui/comments.cljs @@ -0,0 +1,340 @@ +;; 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.ui.comments + (:require + [app.config :as cfg] + [app.main.data.comments :as dcm] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.streams :as ms] + [app.main.ui.context :as ctx] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.data.modal :as modal] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.util.time :as dt] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.i18n :as i18n :refer [t tr]] + [cuerdas.core :as str] + [okulary.core :as l] + [rumext.alpha :as mf])) + +(mf/defc resizing-textarea + {::mf/wrap-props false} + [props] + (let [value (obj/get props "value" "") + on-focus (obj/get props "on-focus") + on-blur (obj/get props "on-blur") + placeholder (obj/get props "placeholder") + on-change (obj/get props "on-change") + on-esc (obj/get props "on-esc") + ref (mf/use-ref) + + on-key-down + (mf/use-callback + (fn [event] + (when (and (kbd/esc? event) + (fn? on-esc)) + (on-esc event)))) + + on-change* + (mf/use-callback + (mf/deps on-change) + (fn [event] + (let [content (dom/get-target-val event)] + (on-change content))))] + + + (mf/use-layout-effect + nil + (fn [] + (let [node (mf/ref-val ref)] + (set! (.-height (.-style node)) "0") + (set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px"))))) + + [:textarea + {:ref ref + :on-key-down on-key-down + :on-focus on-focus + :on-blur on-blur + :value value + :placeholder placeholder + :on-change on-change*}])) + +(mf/defc reply-form + [{:keys [thread] :as props}] + (let [show-buttons? (mf/use-state false) + content (mf/use-state "") + + on-focus + (mf/use-callback + #(reset! show-buttons? true)) + + on-blur + (mf/use-callback + #(reset! show-buttons? false)) + + on-change + (mf/use-callback + #(reset! content %)) + + on-cancel + (mf/use-callback + #(do (reset! content "") + (reset! show-buttons? false))) + + on-submit + (mf/use-callback + (mf/deps thread @content) + (fn [] + (st/emit! (dcm/add-comment thread @content)) + (on-cancel)))] + + [:div.reply-form + [:& resizing-textarea {:value @content + :placeholder "Reply" + :on-blur on-blur + :on-focus on-focus + :on-change on-change}] + (when (or @show-buttons? + (not (empty? @content))) + [:div.buttons + [:input.btn-primary {:type "button" :value "Post" :on-click on-submit}] + [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])])) + +(mf/defc draft-thread + [{:keys [draft zoom on-cancel on-submit] :as props}] + (let [position (:position draft) + content (:content draft) + pos-x (* (:x position) zoom) + pos-y (* (:y position) zoom) + + on-esc + (mf/use-callback + (mf/deps draft) + (fn [event] + (dom/stop-propagation event) + (if (fn? on-cancel) + (on-cancel) + (st/emit! :interrupt)))) + + on-change + (mf/use-callback + (mf/deps draft) + (fn [content] + (st/emit! (dcm/update-draft-thread {:content content})))) + + on-submit + (mf/use-callback + (mf/deps draft) + (partial on-submit draft))] + + [:* + [:div.thread-bubble + {:style {:top (str pos-y "px") + :left (str pos-x "px")} + :on-click dom/stop-propagation} + [:span "?"]] + [:div.thread-content + {:style {:top (str (- pos-y 14) "px") + :left (str (+ pos-x 14) "px")} + :on-click dom/stop-propagation} + [:div.reply-form + [:& resizing-textarea {:placeholder "Write new comment" + :value (or content "") + :on-esc on-esc + :on-change on-change}] + [:div.buttons + [:input.btn-primary + {:on-click on-submit + :type "button" + :value "Post"}] + [:input.btn-secondary + {:on-click on-esc + :type "button" + :value "Cancel"}]]]]])) + +(mf/defc edit-form + [{:keys [content on-submit on-cancel] :as props}] + (let [content (mf/use-state content) + + on-change + (mf/use-callback + #(reset! content %)) + + on-submit* + (mf/use-callback + (mf/deps @content) + (fn [] (on-submit @content)))] + + [:div.reply-form.edit-form + [:& resizing-textarea {:value @content + :on-change on-change}] + [:div.buttons + [:input.btn-primary {:type "button" :value "Post" :on-click on-submit*}] + [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]])) + +(mf/defc comment-item + [{:keys [comment thread users] :as props}] + (let [profile (get (or users @refs/workspace-users) (:owner-id comment)) + options (mf/use-state false) + edition? (mf/use-state false) + + on-show-options + (mf/use-callback #(reset! options true)) + + on-hide-options + (mf/use-callback #(reset! options false)) + + on-edit-clicked + (mf/use-callback + (fn [] + (reset! options false) + (reset! edition? true))) + + on-delete-comment + (mf/use-callback + (mf/deps comment) + (st/emitf (dcm/delete-comment comment))) + + delete-thread + (mf/use-callback + (mf/deps thread) + (st/emitf (dcm/close-thread) + (dcm/delete-comment-thread thread))) + + + on-delete-thread + (mf/use-callback + (mf/deps thread) + (st/emitf (modal/show + {:type :confirm + :title (tr "modals.delete-comment-thread.title") + :message (tr "modals.delete-comment-thread.message") + :accept-label (tr "modals.delete-comment-thread.accept") + :on-accept delete-thread}))) + + on-submit + (mf/use-callback + (mf/deps comment thread) + (fn [content] + (reset! edition? false) + (st/emit! (dcm/update-comment (assoc comment :content content))))) + + on-cancel + (mf/use-callback #(reset! edition? false)) + + toggle-resolved + (mf/use-callback + (mf/deps thread) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))] + + [:div.comment-container + [:div.comment + [:div.author + [:div.avatar + [:img {:src (cfg/resolve-media-path (:photo profile))}]] + [:div.name + [:div.fullname (:fullname profile)] + [:div.timeago (dt/timeago (:modified-at comment))]] + + (when (some? thread) + [:div.options-resolve {:on-click toggle-resolved} + (if (:is-resolved thread) + [:span i/checkbox-checked] + [:span i/checkbox-unchecked])]) + + [:div.options + [:div.options-icon {:on-click on-show-options} i/actions]]] + + [:div.content + (if @edition? + [:& edit-form {:content (:content comment) + :on-submit on-submit + :on-cancel on-cancel}] + [:span.text (:content comment)])]] + + [:& dropdown {:show @options + :on-close on-hide-options} + [:ul.dropdown.comment-options-dropdown + [:li {:on-click on-edit-clicked} "Edit"] + (if thread + [:li {:on-click on-delete-thread} "Delete thread"] + [:li {:on-click on-delete-comment} "Delete comment"])]]])) + +(defn comments-ref + [{:keys [id] :as thread}] + (l/derived (l/in [:comments id]) st/state)) + +(mf/defc thread-comments + [{:keys [thread zoom users]}] + (let [ref (mf/use-ref) + pos (:position thread) + pos-x (+ (* (:x pos) zoom) 14) + pos-y (- (* (:y pos) zoom) 14) + + comments-ref (mf/use-memo (mf/deps thread) #(comments-ref thread)) + comments-map (mf/deref comments-ref) + comments (->> (vals comments-map) + (sort-by :created-at)) + comment (first comments)] + + (mf/use-effect + (st/emitf (dcm/update-comment-thread-status thread))) + + (mf/use-effect + (mf/deps thread) + (st/emitf (dcm/retrieve-comments (:id thread)))) + + (mf/use-layout-effect + (mf/deps thread comments-map) + (fn [] + (when-let [node (mf/ref-val ref)] + (.scrollIntoViewIfNeeded ^js node)))) + + [:div.thread-content + {:style {:top (str pos-y "px") + :left (str pos-x "px")} + :on-click dom/stop-propagation} + + [:div.comments + [:& comment-item {:comment comment + :users users + :thread thread}] + (for [item (rest comments)] + [:* + [:hr] + [:& comment-item {:comment item :users users}]]) + [:div {:ref ref}]] + [:& reply-form {:thread thread}]])) + +(mf/defc thread-bubble + {::mf/wrap [mf/memo]} + [{:keys [thread zoom open? on-click] :as params}] + (let [pos (:position thread) + pos-x (* (:x pos) zoom) + pos-y (* (:y pos) zoom) + on-click* (fn [event] + (dom/stop-propagation event) + (on-click thread))] + + [:div.thread-bubble + {:style {:top (str pos-y "px") + :left (str pos-x "px")} + :on-mouse-down (fn [event] + (dom/prevent-default event)) + :class (dom/classnames + :resolved (:is-resolved thread) + :unread (pos? (:count-unread-comments thread))) + :on-click on-click*} + [:span (:seqn thread)]])) diff --git a/frontend/src/app/main/ui/components/fullscreen.cljs b/frontend/src/app/main/ui/components/fullscreen.cljs new file mode 100644 index 000000000..c7cf120d2 --- /dev/null +++ b/frontend/src/app/main/ui/components/fullscreen.cljs @@ -0,0 +1,57 @@ +;; 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.ui.components.fullscreen + (:require + [app.util.dom :as dom] + [app.util.webapi :as wapi] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(def fullscreen-context + (mf/create-context)) + +(mf/defc fullscreen-wrapper + [{:keys [children] :as props}] + (let [container (mf/use-ref) + state (mf/use-state (dom/fullscreen?)) + + change + (mf/use-callback + (fn [event] + (let [val (dom/fullscreen?)] + (reset! state val)))) + + manager + (mf/use-memo + (mf/deps @state) + (fn [] + (specify! state + cljs.core/IFn + (-invoke + ([it val] + (if val + (wapi/request-fullscreen (mf/ref-val container)) + (wapi/exit-fullscreen)))))))] + + ;; NOTE: the user interaction with F11 keyboard hot-key does not + ;; emits the `fullscreenchange` event; that event is emmited only + ;; when API is used. There are no way to detect the F11 behavior + ;; in a uniform cross browser way. + + (mf/use-effect + (fn [] + (.addEventListener js/document "fullscreenchange" change) + (fn [] + (.removeEventListener js/document "fullscreenchange" change)))) + + [:div.fulllscreen-wrapper {:ref container :class (dom/classnames :fullscreen @state)} + [:& (mf/provider fullscreen-context) {:value manager} + children]])) + diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 11abc5fdb..7e063282b 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -14,6 +14,7 @@ (def embed-ctx (mf/create-context false)) (def render-ctx (mf/create-context nil)) +(def current-route (mf/create-context nil)) (def current-team-id (mf/create-context nil)) (def current-project-id (mf/create-context nil)) (def current-page-id (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/handoff.cljs similarity index 55% rename from frontend/src/app/main/ui/viewer/handoff.cljs rename to frontend/src/app/main/ui/handoff.cljs index fc06143f4..f2d2ebb44 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/handoff.cljs @@ -7,28 +7,27 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff +(ns app.main.ui.handoff (:require - [rumext.alpha :as mf] - [beicon.core :as rx] - [goog.events :as events] - [okulary.core :as l] [app.common.exceptions :as ex] - [app.util.data :refer [classnames]] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] [app.main.data.viewer :as dv] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.fullscreen :as fs] + [app.main.ui.handoff.left-sidebar :refer [left-sidebar]] + [app.main.ui.handoff.render :refer [render-frame-svg]] + [app.main.ui.handoff.right-sidebar :refer [right-sidebar]] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] [app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] - [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] - [app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] - [app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]]) + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [beicon.core :as rx] + [goog.events :as events] + [okulary.core :as l] + [rumext.alpha :as mf]) (:import goog.events.EventType)) (defn handle-select-frame [frame] @@ -37,7 +36,7 @@ (st/emit! (dv/select-shape (:id frame))))) (mf/defc render-panel - [{:keys [data local index page-id file-id]}] + [{:keys [data state index page-id file-id]}] (let [locale (mf/deref i18n/locale) frames (:frames data []) objects (:objects data) @@ -65,26 +64,23 @@ [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} [:div.handoff-svg-container [:& render-frame-svg {:frame-id (:id frame) - :zoom (:zoom local) + :zoom (:zoom state) :objects objects}]]] [:& right-sidebar {:frame frame :page-id page-id :file-id file-id}]])])) (mf/defc handoff-content - [{:keys [data local index page-id file-id] :as props}] - - (let [container (mf/use-ref) - [toggle-fullscreen fullscreen?] (hooks/use-fullscreen container) - - on-mouse-wheel - (fn [event] - (when (kbd/ctrl? event) - (dom/prevent-default event) - (let [event (.getBrowserEvent ^js event)] - (if (pos? (.-deltaY ^js event)) - (st/emit! dv/decrease-zoom) - (st/emit! dv/increase-zoom))))) + [{:keys [data state index page-id file-id] :as props}] + (let [on-mouse-wheel + (mf/use-callback + (fn [event] + (when (kbd/ctrl? event) + (dom/prevent-default event) + (let [event (.getBrowserEvent ^js event)] + (if (pos? (.-deltaY ^js event)) + (st/emit! dv/decrease-zoom) + (st/emit! dv/increase-zoom)))))) on-mount (fn [] @@ -98,37 +94,39 @@ (mf/use-effect on-mount) (hooks/use-shortcuts dv/shortcuts) - [:div.handoff-layout {:class (classnames :fullscreen fullscreen?) - :ref container} - [:& header {:data data - :toggle-fullscreen toggle-fullscreen - :fullscreen? fullscreen? - :local local - :index index - :screen :handoff}] - [:div.viewer-content - (when (:show-thumbnails local) - [:& thumbnails-panel {:index index - :data data - :screen :handoff}]) - [:& render-panel {:data data - :local local - :index index - :page-id page-id - :file-id file-id}]]])) + [:& fs/fullscreen-wrapper {} + [:div.handoff-layout + [:& header + {:data data + :state state + :index index + :section :handoff}] + [:div.viewer-content + (when (:show-thumbnails state) + [:& thumbnails-panel {:index index + :data data + :screen :handoff}]) + [:& render-panel {:data data + :state state + :index index + :page-id page-id + :file-id file-id}]]]])) (mf/defc handoff - [{:keys [file-id page-id index] :as props}] + [{:keys [file-id page-id index token] :as props}] + (mf/use-effect - (mf/deps file-id page-id) + (mf/deps file-id page-id token) (fn [] (st/emit! (dv/initialize props)))) - (let [data (mf/deref refs/viewer-data) - local (mf/deref refs/viewer-local)] - (when data - [:& handoff-content {:file-id file-id - :page-id page-id - :index index - :local local - :data data}]))) + (let [data (mf/deref refs/viewer-data) + state (mf/deref refs/viewer-local)] + + (when (and data state) + [:& handoff-content + {:file-id file-id + :page-id page-id + :index index + :state state + :data data}]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/handoff/attributes.cljs similarity index 74% rename from frontend/src/app/main/ui/viewer/handoff/attributes.cljs rename to frontend/src/app/main/ui/handoff/attributes.cljs index 4b29485eb..487396f0a 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/handoff/attributes.cljs @@ -7,19 +7,19 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes +(ns app.main.ui.handoff.attributes (:require [rumext.alpha :as mf] [app.util.i18n :as i18n] [app.common.geom.shapes :as gsh] - [app.main.ui.viewer.handoff.exports :refer [exports]] - [app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] - [app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] - [app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] - [app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]] - [app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]] - [app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] - [app.main.ui.viewer.handoff.attributes.text :refer [text-panel]])) + [app.main.ui.handoff.exports :refer [exports]] + [app.main.ui.handoff.attributes.layout :refer [layout-panel]] + [app.main.ui.handoff.attributes.fill :refer [fill-panel]] + [app.main.ui.handoff.attributes.stroke :refer [stroke-panel]] + [app.main.ui.handoff.attributes.shadow :refer [shadow-panel]] + [app.main.ui.handoff.attributes.blur :refer [blur-panel]] + [app.main.ui.handoff.attributes.image :refer [image-panel]] + [app.main.ui.handoff.attributes.text :refer [text-panel]])) (def type->options {:multiple [:fill :stroke :image :text :shadow :blur] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs b/frontend/src/app/main/ui/handoff/attributes/blur.cljs similarity index 96% rename from frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs rename to frontend/src/app/main/ui/handoff/attributes/blur.cljs index 203f5658c..166779332 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/blur.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/blur.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.blur +(ns app.main.ui.handoff.attributes.blur (:require [rumext.alpha :as mf] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs b/frontend/src/app/main/ui/handoff/attributes/common.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs rename to frontend/src/app/main/ui/handoff/attributes/common.cljs index 8fe89b976..f2bcce9a4 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/common.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/common.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.common +(ns app.main.ui.handoff.attributes.common (:require [rumext.alpha :as mf] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/handoff/attributes/fill.cljs similarity index 94% rename from frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs rename to frontend/src/app/main/ui/handoff/attributes/fill.cljs index 8afb6d5ba..cf6a8d48a 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/fill.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.fill +(ns app.main.ui.handoff.attributes.fill (:require [rumext.alpha :as mf] [app.util.i18n :refer [t]] @@ -15,7 +15,7 @@ [app.main.ui.icons :as i] [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]])) (def fill-attributes [:fill-color :fill-color-gradient]) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs b/frontend/src/app/main/ui/handoff/attributes/image.cljs similarity index 97% rename from frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs rename to frontend/src/app/main/ui/handoff/attributes/image.cljs index c4fd0c639..5b63e839e 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/image.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/image.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.image +(ns app.main.ui.handoff.attributes.image (:require [rumext.alpha :as mf] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/handoff/attributes/layout.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs rename to frontend/src/app/main/ui/handoff/attributes/layout.cljs index 3aa984b7b..02175ece7 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/layout.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/layout.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.layout +(ns app.main.ui.handoff.attributes.layout (:require [rumext.alpha :as mf] [cuerdas.core :as str] @@ -49,7 +49,7 @@ [:div.attributes-label (t locale "handoff.attributes.layout.left")] [:div.attributes-value (mth/precision (:x shape) 2) "px"] [:& copy-button {:data (copy-data shape :x)}]]) - + (when (not= (:y shape) 0) [:div.attributes-unit-row [:div.attributes-label (t locale "handoff.attributes.layout.top")] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs b/frontend/src/app/main/ui/handoff/attributes/shadow.cljs similarity index 95% rename from frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs rename to frontend/src/app/main/ui/handoff/attributes/shadow.cljs index 44089cc46..1ddb7b86f 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/shadow.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/shadow.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.shadow +(ns app.main.ui.handoff.attributes.shadow (:require [rumext.alpha :as mf] [cuerdas.core :as str] @@ -16,7 +16,7 @@ [app.main.ui.icons :as i] [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]])) (defn has-shadow? [shape] (:shadow shape)) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/handoff/attributes/stroke.cljs similarity index 95% rename from frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs rename to frontend/src/app/main/ui/handoff/attributes/stroke.cljs index fe26f8be5..14faeb5ec 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/stroke.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.stroke +(ns app.main.ui.handoff.attributes.stroke (:require [rumext.alpha :as mf] [cuerdas.core :as str] @@ -16,7 +16,7 @@ [app.main.ui.icons :as i] [app.util.code-gen :as cg] [app.main.ui.components.copy-button :refer [copy-button]] - [app.main.ui.viewer.handoff.attributes.common :refer [color-row]])) + [app.main.ui.handoff.attributes.common :refer [color-row]])) (defn shape->color [shape] {:color (:stroke-color shape) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs b/frontend/src/app/main/ui/handoff/attributes/text.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs rename to frontend/src/app/main/ui/handoff/attributes/text.cljs index 9c1090f41..81c75388a 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/text.cljs +++ b/frontend/src/app/main/ui/handoff/attributes/text.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.attributes.text +(ns app.main.ui.handoff.attributes.text (:require [rumext.alpha :as mf] [cuerdas.core :as str] @@ -19,7 +19,7 @@ [app.main.fonts :as fonts] [app.main.ui.icons :as i] [app.util.webapi :as wapi] - [app.main.ui.viewer.handoff.attributes.common :refer [color-row]] + [app.main.ui.handoff.attributes.common :refer [color-row]] [app.util.code-gen :as cg] [app.main.store :as st] [app.main.ui.components.copy-button :refer [copy-button]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/code.cljs b/frontend/src/app/main/ui/handoff/code.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/code.cljs rename to frontend/src/app/main/ui/handoff/code.cljs index 19ff75c57..eecdb80f7 100644 --- a/frontend/src/app/main/ui/viewer/handoff/code.cljs +++ b/frontend/src/app/main/ui/handoff/code.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.code +(ns app.main.ui.handoff.code (:require ["js-beautify" :as beautify] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/viewer/handoff/exports.cljs b/frontend/src/app/main/ui/handoff/exports.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/exports.cljs rename to frontend/src/app/main/ui/handoff/exports.cljs index 4dbc303c3..a992b6980 100644 --- a/frontend/src/app/main/ui/viewer/handoff/exports.cljs +++ b/frontend/src/app/main/ui/handoff/exports.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.exports +(ns app.main.ui.handoff.exports (:require [rumext.alpha :as mf] [beicon.core :as rx] @@ -59,7 +59,7 @@ (swap! exports (fn [exports] (let [[before after] (split-at index exports)] (d/concat [] before (rest after))))))) - + on-scale-change (mf/use-callback (mf/deps shape) @@ -68,7 +68,7 @@ value (dom/get-value target) value (d/parse-double value)] (swap! exports assoc-in [index :scale] value)))) - + on-suffix-change (mf/use-callback (mf/deps shape) @@ -76,7 +76,7 @@ (let [target (dom/get-target event) value (dom/get-value target)] (swap! exports assoc-in [index :suffix] value)))) - + on-type-change (mf/use-callback (mf/deps shape) diff --git a/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs b/frontend/src/app/main/ui/handoff/left_sidebar.cljs similarity index 96% rename from frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs rename to frontend/src/app/main/ui/handoff/left_sidebar.cljs index 0c2a4ef82..d44ce15c6 100644 --- a/frontend/src/app/main/ui/viewer/handoff/left_sidebar.cljs +++ b/frontend/src/app/main/ui/handoff/left_sidebar.cljs @@ -7,18 +7,18 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.left-sidebar +(ns app.main.ui.handoff.left-sidebar (:require - [rumext.alpha :as mf] - [okulary.core :as l] [app.common.data :as d] [app.common.uuid :as uuid] - [app.main.store :as st] - [app.util.dom :as dom] [app.main.data.viewer :as dv] + [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] - [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]])) + [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]] + [app.util.dom :as dom] + [okulary.core :as l] + [rumext.alpha :as mf])) (def selected-shapes (l/derived (comp :selected :viewer-local) st/state)) @@ -29,7 +29,7 @@ (defn- make-collapsed-iref [id] #(-> (l/in [:viewer-local :collapsed id]) - (l/derived st/state) )) + (l/derived st/state))) (mf/defc layer-item [{:keys [index item selected objects disable-collapse?] :as props}] diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/handoff/render.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/render.cljs rename to frontend/src/app/main/ui/handoff/render.cljs index 7f5c238dc..26e0c8d5d 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/handoff/render.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.render +(ns app.main.ui.handoff.render "The main container for a frame in handoff mode" (:require [rumext.alpha :as mf] @@ -30,7 +30,7 @@ [app.main.ui.shapes.path :as path] [app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.text :as text] - [app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]] + [app.main.ui.handoff.selection-feedback :refer [selection-feedback]] [app.main.ui.shapes.shape :refer [shape-container]])) (declare shape-container-factory) diff --git a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs b/frontend/src/app/main/ui/handoff/right_sidebar.cljs similarity index 94% rename from frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs rename to frontend/src/app/main/ui/handoff/right_sidebar.cljs index 210503d6c..ce9abf05c 100644 --- a/frontend/src/app/main/ui/viewer/handoff/right_sidebar.cljs +++ b/frontend/src/app/main/ui/handoff/right_sidebar.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.right-sidebar +(ns app.main.ui.handoff.right-sidebar (:require [rumext.alpha :as mf] [okulary.core :as l] @@ -16,8 +16,8 @@ [app.main.ui.icons :as i] [app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.workspace.sidebar.layers :refer [element-icon]] - [app.main.ui.viewer.handoff.attributes :refer [attributes]] - [app.main.ui.viewer.handoff.code :refer [code]])) + [app.main.ui.handoff.attributes :refer [attributes]] + [app.main.ui.handoff.code :refer [code]])) (defn make-selected-shapes-iref [] diff --git a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/handoff/selection_feedback.cljs similarity index 98% rename from frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs rename to frontend/src/app/main/ui/handoff/selection_feedback.cljs index ec9c781fc..371134d56 100644 --- a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs +++ b/frontend/src/app/main/ui/handoff/selection_feedback.cljs @@ -7,7 +7,7 @@ ;; ;; Copyright (c) 2020 UXBOX Labs SL -(ns app.main.ui.viewer.handoff.selection-feedback +(ns app.main.ui.handoff.selection-feedback (:require [rumext.alpha :as mf] [cuerdas.core :as str] diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 178360b68..ab9953317 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -50,23 +50,6 @@ (fn [] (mousetrap/reset)))) nil) -(defn use-fullscreen - [ref] - (let [state (mf/use-state (dom/fullscreen?)) - change (mf/use-callback #(reset! state (dom/fullscreen?))) - toggle (mf/use-callback (mf/deps @state) - #(let [el (mf/ref-val ref)] - (swap! state not) - (if @state - (wapi/exit-fullscreen) - (wapi/request-fullscreen el))))] - (mf/use-effect - (fn [] - (.addEventListener js/document "fullscreenchange" change) - #(.removeEventListener js/document "fullscreenchange" change))) - - [toggle @state])) - (defn invisible-image [] (let [img (js/Image.) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 21774edd9..6633f236b 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -9,31 +9,182 @@ (ns app.main.ui.viewer (:require + [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as geom] + [app.common.pages-helpers :as cph] [app.main.data.viewer :as dv] + [app.main.data.comments :as dcm] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.fullscreen :as fs] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] [app.main.ui.viewer.header :refer [header]] - [app.main.ui.viewer.shapes :refer [frame-svg]] + [app.main.ui.viewer.shapes :as shapes :refer [frame-svg]] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] - [app.util.data :refer [classnames]] + [app.main.ui.comments :as cmt] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t tr]] - [beicon.core :as rx] [goog.events :as events] [okulary.core :as l] - [rumext.alpha :as mf]) - (:import goog.events.EventType)) + [rumext.alpha :as mf])) + +(defn- frame-contains? + [{:keys [x y width height]} {px :x py :y}] + (let [x2 (+ x width) + y2 (+ y height)] + (and (<= x px x2) + (<= y py y2)))) + +(def threads-ref + (l/derived :comment-threads st/state)) + +(def comments-local-ref + (l/derived :comments-local st/state)) + +(mf/defc comments-layer + [{:keys [width height zoom frame data] :as props}] + (let [profile (mf/deref refs/profile) + + modifier1 (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + modifier2 (-> (gpt/point (:x frame) (:y frame)) + (gmt/translate-matrix)) + + threads-map (->> (mf/deref threads-ref) + (d/mapm #(update %2 :position gpt/transform modifier1))) + + cstate (mf/deref refs/comments-local) + + mframe (geom/transform-shape frame) + threads (->> (vals threads-map) + (dcm/apply-filters cstate profile) + (filter (fn [{:keys [seqn position]}] + (frame-contains? mframe position)))) + + on-bubble-click + (mf/use-callback + (mf/deps cstate) + (fn [thread] + (if (= (:open cstate) (:id thread)) + (st/emit! (dcm/close-thread)) + (st/emit! (dcm/open-thread thread))))) + + on-click + (mf/use-callback + (mf/deps cstate data) + (fn [event] + (dom/stop-propagation event) + (if (some? (:open cstate)) + (st/emit! (dcm/close-thread)) + (let [event (.-nativeEvent ^js event) + position (-> (dom/get-offset-position event) + (gpt/transform modifier2)) + params {:position position + :page-id (get-in data [:page :id]) + :file-id (get-in data [:file :id])}] + (st/emit! (dcm/create-draft params)))))) + + on-draft-cancel + (mf/use-callback + (mf/deps cstate) + (st/emitf (dcm/close-thread))) + + on-draft-submit + (mf/use-callback + (fn [draft] + (let [params (update draft :position gpt/transform modifier2)] + ;; (prn "on-draft-submit" params) + + (st/emit! (dcm/create-thread params) + (dcm/close-thread))))) + ] + + [:div.viewer-comments {:on-click on-click} + [:div.comments-layer + [:div.threads + (for [item threads] + [:& cmt/thread-bubble {:thread item + :zoom zoom + :on-click on-bubble-click + :open? (= (:id item) (:open cstate)) + :key (:seqn item)}]) + + (when-let [id (:open cstate)] + (when-let [thread (get threads-map id)] + [:& cmt/thread-comments {:thread thread + :users (:users data) + :zoom zoom}])) + + (when-let [draft (:draft cstate)] + [:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1) + :on-cancel on-draft-cancel + :on-submit on-draft-submit + :zoom zoom}])]]])) + + + +(mf/defc viewport + {::mf/wrap [mf/memo]} + [{:keys [state data index section] :or {zoom 1} :as props}] + (let [zoom (:zoom state) + objects (:objects data) + + frame (get-in data [:frames index]) + frame-id (:id frame) + + modifier (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) + + objects (->> (d/concat [frame-id] (cph/get-children frame-id objects)) + (reduce update-fn objects)) + + interactions? (:interactions-show? state) + wrapper (mf/use-memo (mf/deps objects) #(shapes/frame-container-factory objects interactions?)) + + ;; Retrieve frame again with correct modifier + frame (get objects frame-id) + + width (* (:width frame) zoom) + height (* (:height frame) zoom) + vbox (str "0 0 " (:width frame 0) " " (:height frame 0))] + + [:div.viewport-container + {:style {:width width + :height height + :state state + :position "relative"}} + + (when (= section :comments) + [:& comments-layer {:width width + :height height + :frame frame + :data data + :zoom zoom}]) + + [:svg {:view-box vbox + :width width + :height height + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + [:& wrapper {:shape frame + :show-interactions? interactions? + :view-box vbox}]]])) (mf/defc main-panel - [{:keys [data local index]}] + [{:keys [data state index section]}] (let [locale (mf/deref i18n/locale) - frames (:frames data []) - objects (:objects data) + frames (:frames data) frame (get frames index)] [:section.viewer-preview (cond @@ -45,22 +196,20 @@ [:section.empty-state [:span (t locale "viewer.frame-not-found")]] - :else - [:& frame-svg {:frame frame - :show-interactions? (:show-interactions? local) - :zoom (:zoom local) - :objects objects}])])) + (some? state) + [:& viewport + {:data data + :section section + :index index + :state state + }])])) (mf/defc viewer-content - [{:keys [data local index] :as props}] - (let [container (mf/use-ref) - - [toggle-fullscreen fullscreen?] (hooks/use-fullscreen container) - - on-click + [{:keys [data state index section] :as props}] + (let [on-click (fn [event] (dom/stop-propagation event) - (let [mode (get local :interactions-mode)] + (let [mode (get state :interactions-mode)] (when (= mode :show-on-click) (st/emit! dv/flash-interactions)))) @@ -73,49 +222,63 @@ (st/emit! dv/decrease-zoom) (st/emit! dv/increase-zoom))))) + on-click + (fn [event] + (st/emit! (dcm/close-thread))) + + on-key-down + (fn [event] + (when (kbd/esc? event) + (st/emit! (dcm/close-thread)))) + on-mount (fn [] ;; bind with passive=false to allow the event to be cancelled ;; https://stackoverflow.com/a/57582286/3219895 - (let [key1 (events/listen goog/global EventType.WHEEL - on-mouse-wheel #js {"passive" false})] + (let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false}) + key2 (events/listen js/document "keydown" on-key-down) + key3 (events/listen js/document "click" on-click)] (fn [] - (events/unlistenByKey key1))))] + (events/unlistenByKey key1) + (events/unlistenByKey key2) + (events/unlistenByKey key3))))] (mf/use-effect on-mount) (hooks/use-shortcuts dv/shortcuts) - [:div.viewer-layout {:class (classnames :fullscreen fullscreen?) - :ref container} + [:& fs/fullscreen-wrapper {} + [:div.viewer-layout + [:& header + {:data data + :state state + :section section + :index index}] - [:& header {:data data - :toggle-fullscreen toggle-fullscreen - :fullscreen? fullscreen? - :local local - :index index - :screen :viewer}] - [:div.viewer-content {:on-click on-click} - (when (:show-thumbnails local) - [:& thumbnails-panel {:screen :viewer - :index index - :data data}]) - [:& main-panel {:data data - :local local - :index index}]]])) + [:div.viewer-content {:on-click on-click} + (when (:show-thumbnails state) + [:& thumbnails-panel {:screen :viewer + :index index + :data data}]) + [:& main-panel {:data data + :section section + :state state + :index index}]]]])) ;; --- Component: Viewer Page (mf/defc viewer-page - [{:keys [file-id page-id index token] :as props}] + [{:keys [file-id page-id index token section] :as props}] + (mf/use-effect (mf/deps file-id page-id token) - (fn [] - (st/emit! (dv/initialize props)))) + (st/emitf (dv/initialize props))) - (let [data (mf/deref refs/viewer-data) - local (mf/deref refs/viewer-local)] - (when data - [:& viewer-content {:index index - :local local - :data data}]))) + (let [data (mf/deref refs/viewer-data) + state (mf/deref refs/viewer-local)] + (when (and data state) + [:& viewer-content + {:index index + :section section + :state state + :data data}]))) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 62bcb539a..90aabdc95 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -9,21 +9,23 @@ (ns app.main.ui.viewer.header (:require - [rumext.alpha :as mf] - [cuerdas.core :as str] - [app.main.ui.icons :as i] + [app.common.math :as mth] + [app.common.uuid :as uuid] [app.main.data.messages :as dm] [app.main.data.viewer :as dv] + [app.main.data.comments :as dcm] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.fullscreen :as fs] + [app.main.ui.icons :as i] [app.util.data :refer [classnames]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t]] [app.util.router :as rt] - [app.common.math :as mth] - [app.common.uuid :as uuid] - [app.util.webapi :as wapi])) + [app.util.webapi :as wapi] + [cuerdas.core :as str] + [rumext.alpha :as mf])) (mf/defc zoom-widget {:wrap [mf/memo]} @@ -40,7 +42,7 @@ [:span.dropdown-button i/arrow-down] [:& dropdown {:show @show-dropdown? :on-close #(reset! show-dropdown? false)} - [:ul.zoom-dropdown + [:ul.dropdown.zoom-dropdown [:li {:on-click on-increase} "Zoom in" [:span "+"]] [:li {:on-click on-decrease} @@ -52,29 +54,6 @@ [:li {:on-click on-zoom-to-200} "Zoom to 200%" [:span "Shift + 2"]]]]])) -(mf/defc interactions-menu - [{:keys [interactions-mode] :as props}] - (let [show-dropdown? (mf/use-state false) - locale (i18n/use-locale) - on-select-mode #(st/emit! (dv/set-interactions-mode %))] - [:div.header-icon - [:a {:on-click #(swap! show-dropdown? not)} i/eye - [:& dropdown {:show @show-dropdown? - :on-close #(swap! show-dropdown? not)} - [:ul.custom-select-dropdown - [:li {:key :hide - :class (classnames :selected (= interactions-mode :hide)) - :on-click #(on-select-mode :hide)} - (t locale "viewer.header.dont-show-interactions")] - [:li {:key :show - :class (classnames :selected (= interactions-mode :show)) - :on-click #(on-select-mode :show)} - (t locale "viewer.header.show-interactions")] - [:li {:key :show-on-click - :class (classnames :selected (= interactions-mode :show-on-click)) - :on-click #(on-select-mode :show-on-click)} - (t locale "viewer.header.show-interactions-on-click")]]]]])) - (mf/defc share-link [{:keys [page token] :as props}] (let [show-dropdown? (mf/use-state false) @@ -103,7 +82,7 @@ [:& dropdown {:show @show-dropdown? :on-close #(swap! show-dropdown? not) :container dropdown-ref} - [:div.share-link-dropdown {:ref dropdown-ref} + [:div.dropdown.share-link-dropdown {:ref dropdown-ref} [:span.share-link-title (t locale "viewer.header.share.title")] [:div.share-link-input (if (string? token) @@ -121,34 +100,122 @@ [:button.btn-primary {:on-click create} (t locale "viewer.header.share.create-link")])]]]])) +(mf/defc interactions-menu + [{:keys [state locale] :as props}] + (let [imode (:interactions-mode state) + + show-dropdown? (mf/use-state false) + show-dropdown (mf/use-fn #(reset! show-dropdown? true)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + select-mode + (mf/use-callback + (fn [mode] + (st/emit! (dv/set-interactions-mode mode))))] + + [:div.view-options + [:div.icon {:on-click #(swap! show-dropdown? not)} i/eye] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + [:li {:class (dom/classnames :selected (= imode :hide)) + :on-click #(select-mode :hide)} + [:span.icon i/tick] + [:span.label (t locale "viewer.header.dont-show-interactions")]] + + [:li {:class (dom/classnames :selected (= imode :show)) + :on-click #(select-mode :show)} + [:span.icon i/tick] + [:span.label (t locale "viewer.header.show-interactions")]] + + [:li {:class (dom/classnames :selected (= imode :show-on-click)) + :on-click #(select-mode :show-on-click)} + [:span.icon i/tick] + [:span.label (t locale "viewer.header.show-interactions-on-click")]]]]])) + + +(mf/defc comments-menu + [{:keys [locale] :as props}] + (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) + + show-dropdown? (mf/use-state false) + show-dropdown (mf/use-fn #(reset! show-dropdown? true)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + + update-mode + (mf/use-callback + (fn [mode] + (st/emit! (dcm/update-filters {:mode mode})))) + + update-show + (mf/use-callback + (fn [mode] + (st/emit! (dcm/update-filters {:show mode}))))] + + [:div.view-options + [:div.icon {:on-click #(swap! show-dropdown? not)} i/eye] + [:& dropdown {:show @show-dropdown? + :on-close hide-dropdown} + [:ul.dropdown.with-check + [:li {:class (dom/classnames :selected (= :all cmode)) + :on-click #(update-mode :all)} + [:span.icon i/tick] + [:span.label (t locale "labels.show-all-comments")]] + + [:li {:class (dom/classnames :selected (= :yours cmode)) + :on-click #(update-mode :yours)} + [:span.icon i/tick] + [:span.label (t locale "labels.show-your-comments")]] + + [:hr] + + [:li {:class (dom/classnames :selected (= :pending cshow)) + :on-click #(update-show (if (= :pending cshow) :all :pending))} + [:span.icon i/tick] + [:span.label (t locale "labels.hide-resolved-comments")]]]]])) + (mf/defc header - [{:keys [data index local fullscreen? toggle-fullscreen screen] :as props}] + [{:keys [data index section state] :as props}] (let [{:keys [project file page frames]} data - total (count frames) - on-click #(st/emit! dv/toggle-thumbnails-panel) - interactions-mode (:interactions-mode local) - - locale (i18n/use-locale) - - profile (mf/deref refs/profile) + fullscreen (mf/use-ctx fs/fullscreen-context) + total (count frames) + locale (mf/deref i18n/locale) + profile (mf/deref refs/profile) anonymous? (= uuid/zero (:id profile)) project-id (get-in data [:project :id]) - file-id (get-in data [:file :id]) - page-id (get-in data [:page :id]) + file-id (get-in data [:file :id]) + page-id (get-in data [:page :id]) - on-edit #(st/emit! (rt/nav :workspace - {:project-id project-id - :file-id file-id} - {:page-id page-id})) + on-click + (mf/use-callback + (st/emitf dv/toggle-thumbnails-panel)) + + on-edit + (mf/use-callback + (st/emitf (rt/nav :workspace + {:project-id project-id + :file-id file-id} + {:page-id page-id}))) + navigate + (mf/use-callback + (fn [section] + (st/emit! + (case section + :interactions + (rt/nav :viewer + {:file-id file-id :page-id page-id} + {:index index :section "interactions"}) + :comments + (rt/nav :viewer + {:file-id file-id :page-id page-id} + {:index index :section "comments"}) + :handoff + (rt/nav :handoff + {:file-id file-id :page-id page-id} + {:index index})))))] - change-screen - (fn [screen] - (st/emit! - (rt/nav screen - {:file-id file-id :page-id page-id} - {:index index})))] [:header.viewer-header [:div.main-icon [:a {:on-click on-edit} i/logo-icon]] @@ -160,19 +227,33 @@ [:span.file-name (:name file)] [:span "/"] [:span.page-name (:name page)] - [:span.dropdown-button i/arrow-down] + [:span.show-thumbnails-button i/arrow-down] [:span.counters (str (inc index) " / " total)]] [:div.mode-zone - [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :viewer) - (change-screen :viewer)) - :class (when (= screen :viewer) "active") :alt "View mode"} i/play] - [:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :handoff) - (change-screen :handoff)) - :class (when (= screen :handoff) "active") :alt "Code mode"} i/code]] + [:button.mode-zone-button.tooltip.tooltip-bottom + {:on-click #(navigate :interactions) + :class (dom/classnames :active (= section :interactions)) + :alt "View mode"} + i/play] + + [:button.mode-zone-button.tooltip.tooltip-bottom + {:on-click #(navigate :comments) + :class (dom/classnames :active (= section :comments)) + :alt "Comments"} + i/chat] + + [:button.mode-zone-button.tooltip.tooltip-bottom + {:on-click #(navigate :handoff) + :class (dom/classnames :active (= section :handoff)) + :alt "Code mode"} + i/code]] [:div.options-zone - [:& interactions-menu {:interactions-mode interactions-mode}] + (case section + :interactions [:& interactions-menu {:state state :locale locale}] + :comments [:& comments-menu {:locale locale}] + nil) (when-not anonymous? [:& share-link {:token (:share-token data) @@ -183,17 +264,17 @@ (t locale "viewer.header.edit-page")]) [:& zoom-widget - {:zoom (:zoom local) - :on-increase #(st/emit! dv/increase-zoom) - :on-decrease #(st/emit! dv/decrease-zoom) - :on-zoom-to-50 #(st/emit! dv/zoom-to-50) - :on-zoom-to-100 #(st/emit! dv/reset-zoom) - :on-zoom-to-200 #(st/emit! dv/zoom-to-200)}] + {:zoom (:zoom state) + :on-increase (st/emitf dv/increase-zoom) + :on-decrease (st/emitf dv/decrease-zoom) + :on-zoom-to-50 (st/emitf dv/zoom-to-50) + :on-zoom-to-100 (st/emitf dv/reset-zoom) + :on-zoom-to-200 (st/emitf dv/zoom-to-200)}] [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom {:alt (t locale "viewer.header.fullscreen") - :on-click toggle-fullscreen} - (if fullscreen? + :on-click #(if @fullscreen (fullscreen false) (fullscreen true))} + (if @fullscreen i/full-screen-off i/full-screen)] ]])) diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 57fb01152..b2af4a515 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -111,8 +111,7 @@ on-item-click (fn [event index] (compare-and-set! selected false true) - (st/emit! (rt/nav screen {:file-id file-id - :page-id page-id} {:index index})) + (st/emit! (dv/go-to-frame-by-index index)) (when @expanded? (on-close)))] [:& dropdown' {:on-close on-close diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index dd9860298..f2f113e88 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -12,362 +12,54 @@ [app.config :as cfg] [app.main.data.workspace :as dw] [app.main.data.workspace.comments :as dwcm] - [app.main.data.workspace.common :as dwc] + [app.main.data.comments :as dcm] [app.main.refs :as refs] [app.main.store :as st] - [app.main.streams :as ms] [app.main.ui.context :as ctx] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.data.modal :as modal] - [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] - [app.main.ui.keyboard :as kbd] - [app.main.ui.workspace.colorpicker] - [app.main.ui.workspace.context-menu :refer [context-menu]] + [app.main.ui.comments :as cmt] [app.util.time :as dt] [app.util.timers :as tm] [app.util.dom :as dom] - [app.util.object :as obj] - [beicon.core :as rx] [app.util.i18n :as i18n :refer [t tr]] [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) -(declare group-threads-by-page) -(declare apply-filters) - -(mf/defc resizing-textarea - {::mf/wrap-props false} - [props] - (let [value (obj/get props "value" "") - on-focus (obj/get props "on-focus") - on-blur (obj/get props "on-blur") - placeholder (obj/get props "placeholder") - on-change (obj/get props "on-change") - - on-esc (obj/get props "on-esc") - - ref (mf/use-ref) - ;; state (mf/use-state value) - - on-key-down - (mf/use-callback - (fn [event] - (when (and (kbd/esc? event) - (fn? on-esc)) - (on-esc event)))) - - on-change* - (mf/use-callback - (mf/deps on-change) - (fn [event] - (let [content (dom/get-target-val event)] - (on-change content))))] - - - (mf/use-layout-effect - nil - (fn [] - (let [node (mf/ref-val ref)] - (set! (.-height (.-style node)) "0") - (set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px"))))) - - [:textarea - {:ref ref - :on-key-down on-key-down - :on-focus on-focus - :on-blur on-blur - :value value - :placeholder placeholder - :on-change on-change*}])) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Workspace -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(mf/defc reply-form - [{:keys [thread] :as props}] - (let [show-buttons? (mf/use-state false) - content (mf/use-state "") - - on-focus - (mf/use-callback - #(reset! show-buttons? true)) - - on-blur - (mf/use-callback - #(reset! show-buttons? false)) - - on-change - (mf/use-callback - #(reset! content %)) - - on-cancel - (mf/use-callback - #(do (reset! content "") - (reset! show-buttons? false))) - - on-submit - (mf/use-callback - (mf/deps thread @content) - (fn [] - (st/emit! (dwcm/add-comment thread @content)) - (on-cancel)))] - - [:div.reply-form - [:& resizing-textarea {:value @content - :placeholder "Reply" - :on-blur on-blur - :on-focus on-focus - :on-change on-change}] - (when (or @show-buttons? - (not (empty? @content))) - [:div.buttons - [:input.btn-primary {:type "button" :value "Post" :on-click on-submit}] - [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])])) - -(mf/defc draft-thread - [{:keys [draft zoom] :as props}] - (let [position (:position draft) - content (:content draft) - pos-x (* (:x position) zoom) - pos-y (* (:y position) zoom) - - on-esc - (mf/use-callback - (mf/deps draft) - (st/emitf :interrupt)) - - on-change - (mf/use-callback - (mf/deps draft) - (fn [content] - (st/emit! (dwcm/update-draft-thread (assoc draft :content content))))) - - on-submit - (mf/use-callback - (mf/deps draft) - (st/emitf (dwcm/create-thread draft)))] - - [:* - [:div.thread-bubble - {:style {:top (str pos-y "px") - :left (str pos-x "px")}} - [:span "?"]] - [:div.thread-content - {:style {:top (str (- pos-y 14) "px") - :left (str (+ pos-x 14) "px")}} - [:div.reply-form - [:& resizing-textarea {:placeholder "Write new comment" - :value content - :on-esc on-esc - :on-change on-change}] - [:div.buttons - [:input.btn-primary - {:on-click on-submit - :type "button" - :value "Post"}] - [:input.btn-secondary - {:on-click on-esc - :type "button" - :value "Cancel"}]]]]])) - - -(mf/defc edit-form - [{:keys [content on-submit on-cancel] :as props}] - (let [content (mf/use-state content) - - on-change - (mf/use-callback - #(reset! content %)) - - on-submit* - (mf/use-callback - (mf/deps @content) - (fn [] (on-submit @content)))] - - [:div.reply-form.edit-form - [:& resizing-textarea {:value @content - :on-change on-change}] - [:div.buttons - [:input.btn-primary {:type "button" :value "Post" :on-click on-submit*}] - [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]])) - - -(mf/defc comment-item - [{:keys [comment thread] :as props}] - (let [profile (get @refs/workspace-users (:owner-id comment)) - - options (mf/use-state false) - edition? (mf/use-state false) - - on-show-options - (mf/use-callback #(reset! options true)) - - on-hide-options - (mf/use-callback #(reset! options false)) - - on-edit-clicked - (mf/use-callback - (fn [] - (reset! options false) - (reset! edition? true))) - - on-delete-comment - (mf/use-callback - (mf/deps comment) - (st/emitf (dwcm/delete-comment comment))) - - delete-thread - (mf/use-callback - (mf/deps thread) - (st/emitf (dwcm/close-thread) - (dwcm/delete-comment-thread thread))) - - - on-delete-thread - (mf/use-callback - (mf/deps thread) - (st/emitf (modal/show - {:type :confirm - :title (tr "modals.delete-comment-thread.title") - :message (tr "modals.delete-comment-thread.message") - :accept-label (tr "modals.delete-comment-thread.accept") - :on-accept delete-thread}))) - - on-submit - (mf/use-callback - (mf/deps comment thread) - (fn [content] - (reset! edition? false) - (st/emit! (dwcm/update-comment (assoc comment :content content))))) - - on-cancel - (mf/use-callback #(reset! edition? false)) - - toggle-resolved - (mf/use-callback - (mf/deps thread) - (st/emitf (dwcm/update-comment-thread (update thread :is-resolved not))))] - - [:div.comment-container - [:div.comment - [:div.author - [:div.avatar - [:img {:src (cfg/resolve-media-path (:photo profile))}]] - [:div.name - [:div.fullname (:fullname profile)] - [:div.timeago (dt/timeago (:modified-at comment))]] - - (when (some? thread) - [:div.options-resolve {:on-click toggle-resolved} - (if (:is-resolved thread) - [:span i/checkbox-checked] - [:span i/checkbox-unchecked])]) - - [:div.options - [:div.options-icon {:on-click on-show-options} i/actions]]] - - [:div.content - (if @edition? - [:& edit-form {:content (:content comment) - :on-submit on-submit - :on-cancel on-cancel}] - [:span.text (:content comment)])]] - - [:& dropdown {:show @options - :on-close on-hide-options} - [:ul.dropdown.comment-options-dropdown - [:li {:on-click on-edit-clicked} "Edit"] - (if thread - [:li {:on-click on-delete-thread} "Delete thread"] - [:li {:on-click on-delete-comment} "Delete comment"])]]])) - -(defn comments-ref - [{:keys [id] :as thread}] - (l/derived (l/in [:comments id]) st/state)) - -(mf/defc thread-comments - [{:keys [thread zoom]}] - (let [ref (mf/use-ref) - pos (:position thread) - pos-x (+ (* (:x pos) zoom) 14) - pos-y (- (* (:y pos) zoom) 14) - - comments-ref (mf/use-memo (mf/deps thread) #(comments-ref thread)) - comments-map (mf/deref comments-ref) - comments (->> (vals comments-map) - (sort-by :created-at)) - comment (first comments)] - - (mf/use-effect - (st/emitf (dwcm/update-comment-thread-status thread))) - - (mf/use-effect - (mf/deps thread) - (st/emitf (dwcm/retrieve-comments (:id thread)))) - - (mf/use-layout-effect - (mf/deps thread comments-map) - (fn [] - (when-let [node (mf/ref-val ref)] - (.scrollIntoView ^js node)))) - - [:div.thread-content - {:style {:top (str pos-y "px") - :left (str pos-x "px")}} - - [:div.comments - [:& comment-item {:comment comment - :thread thread}] - (for [item (rest comments)] - [:* - [:hr] - [:& comment-item {:comment item}]]) - [:div {:ref ref}]] - [:& reply-form {:thread thread}]])) - -(mf/defc thread-bubble - {::mf/wrap [mf/memo]} - [{:keys [thread zoom open?] :as params}] - (let [pos (:position thread) - pos-x (* (:x pos) zoom) - pos-y (* (:y pos) zoom) - - on-open-toggle - (mf/use-callback - (mf/deps thread open?) - (fn [] - (if open? - (st/emit! (dwcm/close-thread)) - (st/emit! (dwcm/open-thread thread)))))] - - [:div.thread-bubble - {:style {:top (str pos-y "px") - :left (str pos-x "px")} - :class (dom/classnames - :resolved (:is-resolved thread) - :unread (pos? (:count-unread-comments thread))) - :on-click on-open-toggle} - [:span (:seqn thread)]])) - (def threads-ref (l/derived :comment-threads st/state)) -(def workspace-comments-ref - (l/derived :workspace-comments st/state)) - (mf/defc comments-layer [{:keys [vbox vport zoom file-id page-id drawing] :as props}] (let [pos-x (* (- (:x vbox)) zoom) pos-y (* (- (:y vbox)) zoom) + profile (mf/deref refs/profile) - local (mf/deref workspace-comments-ref) + local (mf/deref refs/comments-local) threads-map (mf/deref threads-ref) + threads (->> (vals threads-map) (filter #(= (:page-id %) page-id)) - (apply-filters local profile))] + (dcm/apply-filters local profile)) + + on-bubble-click + (fn [{:keys [id] :as thread}] + (if (= (:open local) id) + (st/emit! (dcm/close-thread)) + (st/emit! (dcm/open-thread thread)))) + + on-draft-cancel + (mf/use-callback + (st/emitf :interrupt)) + + on-draft-submit + (mf/use-callback + (fn [draft] + (st/emit! (dcm/create-thread draft) + #_(dcm/close-thread)))) + ] + (mf/use-effect (mf/deps file-id) @@ -377,22 +69,27 @@ (st/emit! ::dwcm/finalize)))) [:div.workspace-comments - {:style {:width (str (:width vport) "px") - :height (str (:height vport) "px")}} - [:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} - (for [item threads] - [:& thread-bubble {:thread item - :zoom zoom - :open? (= (:id item) (:open local)) - :key (:seqn item)}]) + [:div.comments-layer + {:style {:width (str (:width vport) "px") + :height (str (:height vport) "px")}} + [:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} + (for [item threads] + [:& cmt/thread-bubble {:thread item + :zoom zoom + :on-click on-bubble-click + :open? (= (:id item) (:open local)) + :key (:seqn item)}]) - (when-let [id (:open local)] - (when-let [thread (get threads-map id)] - [:& thread-comments {:thread thread - :zoom zoom}])) + (when-let [id (:open local)] + (when-let [thread (get threads-map id)] + [:& cmt/thread-comments {:thread thread + :zoom zoom}])) - (when-let [draft (:comment drawing)] - [:& draft-thread {:draft draft :zoom zoom}])]])) + (when-let [draft (:comment drawing)] + [:& cmt/draft-thread {:draft draft + :on-cancel on-draft-cancel + :on-submit on-draft-submit + :zoom zoom}])]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -413,7 +110,7 @@ (st/emit! (dw/go-to-page (:page-id item)))) (tm/schedule (st/emitf (dwcm/center-to-comment-thread item) - (dwcm/open-thread item)))))] + (dcm/open-thread item)))))] [:div.comment {:on-click on-click} [:div.author @@ -461,41 +158,49 @@ (mf/defc sidebar-options [{:keys [local] :as props}] - (let [filter-yours - (mf/use-callback - (mf/deps local) - (st/emitf (dwcm/update-filters {:main :yours}))) + (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) + locale (mf/deref i18n/locale) - filter-all + update-mode (mf/use-callback - (mf/deps local) - (st/emitf (dwcm/update-filters {:main :all}))) + (fn [mode] + (st/emit! (dcm/update-filters {:mode mode})))) - toggle-resolved + update-show (mf/use-callback - (mf/deps local) - (st/emitf (dwcm/update-filters {:resolved (not (:filter-resolved local))})))] + (fn [mode] + (st/emit! (dcm/update-filters {:show mode}))))] - [:ul.dropdown.sidebar-options-dropdown - [:li {:on-click filter-all} "All"] - [:li {:on-click filter-yours} "Only yours"] - [:hr] - (if (:filter-resolved local) - [:li {:on-click toggle-resolved} "Show resolved comments"] - [:li {:on-click toggle-resolved} "Hide resolved comments"])])) + [:ul.dropdown.with-check.sidebar-options-dropdown + [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) + :on-click #(update-mode :all)} + [:span.icon i/tick] + [:span.label (t locale "labels.show-all-comments")]] + + [:li {:class (dom/classnames :selected (= :yours cmode)) + :on-click #(update-mode :yours)} + [:span.icon i/tick] + [:span.label (t locale "labels.show-your-comments")]] + + [:hr] + + [:li {:class (dom/classnames :selected (= :pending cshow)) + :on-click #(update-show (if (= :pending cshow) :all :pending))} + [:span.icon i/tick] + [:span.label (t locale "labels.hide-resolved-comments")]]])) (mf/defc comments-sidebar [] (let [threads-map (mf/deref threads-ref) profile (mf/deref refs/profile) - local (mf/deref workspace-comments-ref) + local (mf/deref refs/comments-local) options? (mf/use-state false) tgroups (->> (vals threads-map) (sort-by :modified-at) (reverse) - (apply-filters local profile) - (group-threads-by-page))] + (dcm/apply-filters local profile) + (dcm/group-threads-by-page))] [:div.workspace-comments.workspace-comments-sidebar [:div.sidebar-title @@ -520,30 +225,3 @@ :key (:page-id tgroup)}]])])])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Helpers -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- group-threads-by-page - [threads] - (letfn [(group-by-page [result thread] - (let [current (first result)] - (if (= (:page-id current) (:page-id thread)) - (cons (update current :items conj thread) - (rest result)) - (cons {:page-id (:page-id thread) :items [thread]} - result))))] - (reverse - (reduce group-by-page nil threads)))) - -(defn- apply-filters - [local profile threads] - (cond->> threads - (true? (:filter-resolved local)) - (filter (fn [item] - (or (not (:is-resolved item)) - (= (:id item) (:open local))))) - - (= :yours (:filter local)) - (filter #(contains? (:participants %) (:id profile))))) - diff --git a/frontend/src/app/util/avatars.cljs b/frontend/src/app/util/avatars.cljs index 39f3908a6..64a79e1ee 100644 --- a/frontend/src/app/util/avatars.cljs +++ b/frontend/src/app/util/avatars.cljs @@ -35,3 +35,9 @@ (.fillText context letters (/ size 2) (/ size 1.5)) (.toDataURL canvas))) + +(defn assoc-profile-avatar + [{:keys [photo fullname] :as profile}] + (cond-> profile + (or (nil? photo) (empty? photo)) + (assoc :photo (generate {:name fullname})))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 92edd6535..c5cdc9c09 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -9,12 +9,11 @@ (ns app.util.dom (:require - [goog.dom :as dom] - [cuerdas.core :as str] - [beicon.core :as rx] - [cuerdas.core :as str] + [app.common.exceptions :as ex] [app.common.geom.point :as gpt] - [app.util.transit :as ts])) + [app.util.object :as obj] + [cuerdas.core :as str] + [goog.dom :as dom])) ;; --- Deprecated methods @@ -111,11 +110,11 @@ (defn select-text! [node] - (.select node)) + (.select ^js node)) (defn ^boolean equals? [node-a node-b] - (.isEqualNode node-a node-b)) + (.isEqualNode ^js node-a node-b)) (defn get-event-files "Extract the files from event instance." @@ -162,6 +161,12 @@ y (.-clientY event)] (gpt/point x y))) +(defn get-offset-position + [event] + (let [x (.-offsetX event) + y (.-offsetY event)] + (gpt/point x y))) + (defn get-client-size [node] {:width (.-clientWidth ^js node) @@ -188,7 +193,16 @@ (defn fullscreen? [] - (boolean (.-fullscreenElement js/document))) + (cond + (obj/in? js/document "webkitFullscreenElement") + (boolean (.-webkitFullscreenElement js/document)) + + (obj/in? js/document "fullscreenElement") + (boolean (.-fullscreenElement js/document)) + + :else + (ex/raise :type :not-supported + :hint "seems like the current browset does not support fullscreen api."))) (defn ^boolean blob? [v] @@ -219,6 +233,6 @@ (defn release-pointer [event] (-> event get-target (.releasePointerCapture (.-pointerId event)))) - + (defn get-root [] (query js/document "#app")) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index a867cc646..25d9b254f 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -76,3 +76,7 @@ (defn clj->props [props] (clj->js props :keyword-fn props-key-fn)) + +(defn ^boolean in? + [obj prop] + (js* "~{} in ~{}" prop obj)) diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index f5b3eb75f..f18cc5608 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -10,6 +10,8 @@ (ns app.util.webapi "HTML5 web api helpers." (:require + [app.common.exceptions :as ex] + [app.util.object :as obj] [promesa.core :as p] [beicon.core :as rx] [cuerdas.core :as str] @@ -97,8 +99,26 @@ (defn request-fullscreen [el] - (.requestFullscreen el)) + (cond + (obj/in? el "requestFullscreen") + (.requestFullscreen el) + + (obj/in? el "webkitRequestFullscreen") + (.webkitRequestFullscreen el) + + :else + (ex/raise :type :not-supported + :hint "seems like the current browset does not support fullscreen api."))) (defn exit-fullscreen [] - (.exitFullscreen js/document)) + (cond + (obj/in? js/document "exitFullscreen") + (.exitFullscreen js/document) + + (obj/in? js/document "webkitExitFullscreen") + (.webkitExitFullscreen js/document) + + :else + (ex/raise :type :not-supported + :hint "seems like the current browset does not support fullscreen api.")))