🎉 Add comments to viewer.

This commit is contained in:
Andrey Antukh 2020-11-18 17:36:14 +01:00 committed by Alonso Torres
parent e1db6d3a37
commit 64a6ba1949
45 changed files with 1629 additions and 1074 deletions

View file

@ -203,6 +203,8 @@
(defn retrieve-file-users (defn retrieve-file-users
[conn id] [conn id]
(->> (db/exec! conn [sql:file-users id 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])))) (mapv #(media/resolve-media-uris % [:photo :photo-uri]))))
(s/def ::file-users (s/def ::file-users

View file

@ -50,8 +50,11 @@
file (merge (dissoc file :data) file (merge (dissoc file :data)
(select-keys (:data file) [:colors :media :typographies])) (select-keys (:data file) [:colors :media :typographies]))
libs (files/retrieve-file-libraries conn false file-id) libs (files/retrieve-file-libraries conn false file-id)
users (files/retrieve-file-users conn file-id)
bundle {:file file bundle {:file file
:page page :page page
:users users
:project project :project project
:libraries libs}] :libraries libs}]
(if (string? token) (if (string? token)

View file

@ -119,6 +119,7 @@
(s/def ::url string?) (s/def ::url string?)
(s/def ::fn fn?) (s/def ::fn fn?)
(s/def ::point gpt/point?) (s/def ::point gpt/point?)
(s/def ::id ::uuid)
;; --- Macros ;; --- Macros

View file

@ -1608,28 +1608,19 @@
"modals.delete-comment-thread.accept" : { "modals.delete-comment-thread.accept" : {
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:236" ], "used-in" : [ "src/app/main/ui/workspace/comments.cljs:236" ],
"translations" : { "translations" : {
"en" : null, "en" : "Delete conversation"
"fr" : null,
"ru" : null,
"es" : null
} }
}, },
"modals.delete-comment-thread.message" : { "modals.delete-comment-thread.message" : {
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:235" ], "used-in" : [ "src/app/main/ui/workspace/comments.cljs:235" ],
"translations" : { "translations" : {
"en" : null, "en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted."
"fr" : null,
"ru" : null,
"es" : null
} }
}, },
"modals.delete-comment-thread.title" : { "modals.delete-comment-thread.title" : {
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:234" ], "used-in" : [ "src/app/main/ui/workspace/comments.cljs:234" ],
"translations" : { "translations" : {
"en" : null, "en" : "Delete conversation"
"fr" : null,
"ru" : null,
"es" : null
} }
}, },
"modals.delete-file-confirm.accept" : { "modals.delete-file-confirm.accept" : {
@ -1881,6 +1872,10 @@
"es" : "No se encuentra el tablero." "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" : { "viewer.header.dont-show-interactions" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ],
"translations" : { "translations" : {

View file

@ -11,7 +11,7 @@
border-color: $color-gray-10; border-color: $color-gray-10;
} }
li { > li {
display: flex; display: flex;
align-items: center; align-items: center;
color: $color-gray-60; color: $color-gray-60;
@ -20,6 +20,12 @@
height: 40px; height: 40px;
padding: 5px 16px; padding: 5px 16px;
svg {
fill: $color-gray-20;
height: 12px;
width: 12px;
}
&.title { &.title {
font-weight: 600; font-weight: 600;
cursor: default; cursor: default;
@ -29,4 +35,27 @@
background-color: $color-primary-lighter; 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;
}
}
} }

View file

@ -42,15 +42,19 @@
} }
} }
.header-icon { .view-options {
.icon {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
a { &:hover {
height: 16px; > svg {
width: 16px; fill: $color-primary;
}
}
}
svg { svg {
fill: $color-gray-30; fill: $color-gray-30;
@ -58,11 +62,10 @@
width: 16px; width: 16px;
} }
&:hover { .dropdown {
svg { top: 40px;
fill: $color-primary; left: 0px;
} width: 260px;
}
} }
} }
@ -92,13 +95,11 @@
} }
} }
.dropdown-button { .show-thumbnails-button svg {
svg {
fill: $color-white; fill: $color-white;
height: 10px; height: 10px;
width: 10px; width: 10px;
} }
}
.page-name { .page-name {
color: $color-white; color: $color-white;
@ -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 { .zoom-dropdown {
left : 116px; left: 180px;
top: 45px; top: 40px;
} }
.users-zone { .users-zone {

View file

@ -7,19 +7,18 @@
} }
.viewer-preview { .viewer-preview {
height: 100vh; height: calc(100vh - 40px);
grid-row: 1 / span 2; grid-row: 1 / span 2;
grid-column: 1 / span 1; grid-column: 1 / span 1;
overflow: scroll; overflow: auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-flow: wrap; flex-flow: wrap;
.empty-state { .empty-state {
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View file

@ -1,4 +1,16 @@
.workspace-comments { .viewer-comments {
width: 100%;
height: 100%;
z-index: 1000;
position: absolute;
top: 0px;
left: 0px;
}
.viewer-comments, .workspace-comments {
.comments-layer {
width: 100%; width: 100%;
height: 100%; height: 100%;
grid-column: 1/span 2; grid-column: 1/span 2;
@ -8,7 +20,10 @@
overflow: hidden; overflow: hidden;
.threads { .threads {
position: relative; position: absolute;
top: 0px;
left: 0px;
}
} }
.thread-bubble { .thread-bubble {
@ -104,8 +119,6 @@
} }
} }
.comment-container { .comment-container {
position: relative; position: relative;
} }
@ -132,7 +145,7 @@
font-size: $fs13; font-size: $fs13;
@include text-ellipsis; @include text-ellipsis;
width: 110px; width: 150px;
} }
.timeago { .timeago {

View file

@ -10,6 +10,7 @@
(ns app.main (ns app.main
(:require (:require
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.spec :as us]
[app.main.data.auth :refer [logout]] [app.main.data.auth :refer [logout]]
[app.main.data.users :as udu] [app.main.data.users :as udu]
[app.main.store :as st] [app.main.store :as st]
@ -35,12 +36,26 @@
(declare reinit) (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 (defn on-navigate
[router path] [router path]
(let [match (rt/match router path) (let [match (match-path router path)
profile (:profile storage) profile (:profile storage)
authed? (and (not (nil? profile)) authed? (and (not (nil? profile))
(not= (:id profile) uuid/zero))] (not= (:id profile) uuid/zero))]
(cond (cond
(and (or (= path "") (and (or (= path "")
(nil? match)) (nil? match))

View file

@ -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))))))

View file

@ -23,7 +23,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.pages-helpers :as cph])) [app.common.pages-helpers :as cph]))
;; --- Specs ;; --- General Specs
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
@ -32,40 +32,63 @@
(s/def ::file (s/keys :req-un [::id ::name])) (s/def ::file (s/keys :req-un [::id ::name]))
(s/def ::page ::cp/page) (s/def ::page ::cp/page)
(s/def ::interactions-mode #{:hide :show :show-on-click})
(s/def ::bundle (s/def ::bundle
(s/keys :req-un [::project ::file ::page])) (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 fetch-bundle)
(declare bundle-fetched) (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 (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/reify ::initialize
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc state :viewer-local {:zoom 1 (update state :viewer-local
:page-id page-id (fn [lstate]
:file-id file-id (if (nil? lstate)
:interactions-mode :hide default-local-state
:show-interactions? false lstate))))
:selected #{}
:collapsed #{}
:hover nil}))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(rx/of (fetch-bundle params))))) (rx/of (fetch-bundle params)
(fetch-comment-threads params)))))
;; --- Data Fetching ;; --- Data Fetching
(s/def ::fetch-bundle-params
(s/keys :req-un [::page-id ::file-id]
:opt-in [::token]))
(defn fetch-bundle (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/reify ::fetch-file
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
@ -76,6 +99,40 @@
(rx/first) (rx/first)
(rx/map bundle-fetched)))))) (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 (defn- extract-frames
[objects] [objects]
(let [root (get objects uuid/zero)] (let [root (get objects uuid/zero)]
@ -86,7 +143,7 @@
(vec)))) (vec))))
(defn bundle-fetched (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) (us/verify ::bundle bundle)
(ptk/reify ::file-fetched (ptk/reify ::file-fetched
ptk/UpdateEvent ptk/UpdateEvent
@ -94,9 +151,10 @@
(let [objects (:objects page) (let [objects (:objects page)
frames (extract-frames objects)] frames (extract-frames objects)]
(-> state (-> state
(assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries)) (assoc :viewer-libraries (d/index-by :id libraries)
:viewer-data {:project project :viewer-data {:project project
:objects objects :objects objects
:users (d/index-by :id users)
:file file :file file
:page page :page page
:frames frames :frames frames
@ -178,9 +236,9 @@
(watch [_ state stream] (watch [_ state stream]
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword) screen (-> route :data :name keyword)
qparams (get-in route [:params :query]) qparams (:query-params route)
pparams (get-in route [:params :path]) pparams (:path-params route)
index (d/parse-integer (:index qparams))] index (:index qparams)]
(when (pos? index) (when (pos? index)
(rx/of (rt/nav screen pparams (assoc qparams :index (dec index))))))))) (rx/of (rt/nav screen pparams (assoc qparams :index (dec index)))))))))
@ -190,13 +248,15 @@
(watch [_ state stream] (watch [_ state stream]
(let [route (:route state) (let [route (:route state)
screen (-> route :data :name keyword) screen (-> route :data :name keyword)
qparams (get-in route [:params :query]) qparams (:query-params route)
pparams (get-in route [:params :path]) pparams (:path-params route)
index (d/parse-integer (:index qparams)) index (:index qparams)
total (count (get-in state [:viewer-data :frames]))] total (count (get-in state [:viewer-data :frames]))]
(when (< index (dec total)) (when (< index (dec total))
(rx/of (rt/nav screen pparams (assoc qparams :index (inc index))))))))) (rx/of (rt/nav screen pparams (assoc qparams :index (inc index)))))))))
(s/def ::interactions-mode #{:hide :show :show-on-click})
(defn set-interactions-mode (defn set-interactions-mode
[mode] [mode]
(us/verify ::interactions-mode mode) (us/verify ::interactions-mode mode)
@ -205,7 +265,7 @@
(update [_ state] (update [_ state]
(-> state (-> state
(assoc-in [:viewer-local :interactions-mode] mode) (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 :hide false
:show true :show true
:show-on-click false)))))) :show-on-click false))))))
@ -216,7 +276,7 @@
(ptk/reify ::flash-interactions (ptk/reify ::flash-interactions
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :show-interactions?] true)) (assoc-in state [:viewer-local :interactions-show?] true))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
@ -229,26 +289,30 @@
(ptk/reify ::flash-done (ptk/reify ::flash-done
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc-in state [:viewer-local :show-interactions?] false)))) (assoc-in state [:viewer-local :interactions-show?] false))))
;; --- Navigation ;; --- 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 (defn go-to-frame
[frame-id] [frame-id]
(us/verify ::us/uuid frame-id) (us/verify ::us/uuid frame-id)
(ptk/reify ::go-to-frame (ptk/reify ::go-to-frame
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [page-id (get-in state [:viewer-local :page-id]) (let [frames (get-in state [:viewer-data :frames])
file-id (get-in state [:viewer-local :file-id])
frames (get-in state [:viewer-data :frames])
token (get-in state [:viewer-data :token])
index (d/index-of-pred frames #(= (:id %) frame-id))] index (d/index-of-pred frames #(= (:id %) frame-id))]
(rx/of (rt/nav :viewer (rx/of (go-to-frame-by-index index))))))
{:page-id page-id
:file-id file-id}
{:token token
:index index}))))))
(defn set-current-frame [frame-id] (defn set-current-frame [frame-id]
(ptk/reify ::current-frame (ptk/reify ::current-frame

View file

@ -144,6 +144,8 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(assoc state (assoc state
:current-file-id file-id
:current-project-id project-id
:workspace-presence {})) :workspace-presence {}))
ptk/WatchEvent ptk/WatchEvent

View file

@ -9,59 +9,34 @@
(ns app.main.data.workspace.comments (ns app.main.data.workspace.comments
(:require (:require
[cuerdas.core :as str]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [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.math :as mth]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.constants :as c] [app.main.constants :as c]
[app.main.data.workspace.common :as dwc] [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.store :as st]
[app.main.streams :as ms] [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] [beicon.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[clojure.set :as set]
[potok.core :as ptk])) [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-interrupt)
(declare handle-comment-layer-click) (declare handle-comment-layer-click)
(defn initialize-comments (defn initialize-comments
[file-id] [file-id]
(us/assert ::us/uuid file-id) (us/assert ::us/uuid file-id)
(ptk/reify ::start-commenting (ptk/reify ::initialize-comments
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local assoc :commenting true))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stoper (rx/filter #(= ::finalize %) stream)] (let [stoper (rx/filter #(= ::finalize %) stream)]
(rx/merge (rx/merge
(rx/of (retrieve-comment-threads file-id)) (rx/of (dcm/retrieve-comment-threads file-id))
(->> stream (->> stream
(rx/filter ms/mouse-click?) (rx/filter ms/mouse-click?)
(rx/switch-map #(rx/take 1 ms/mouse-position)) (rx/switch-map #(rx/take 1 ms/mouse-position))
(rx/mapcat #(rx/take 1 ms/mouse-position))
(rx/map handle-comment-layer-click) (rx/map handle-comment-layer-click)
(rx/take-until stoper)) (rx/take-until stoper))
(->> stream (->> stream
@ -72,19 +47,13 @@
(defn- handle-interrupt (defn- handle-interrupt
[] []
(ptk/reify ::handle-interrupt (ptk/reify ::handle-interrupt
ptk/UpdateEvent ptk/WatchEvent
(update [_ state] (watch [_ state stream]
(let [local (:workspace-comments state) (let [local (:comments-local state)]
drawing (:workspace-drawing state)]
(cond (cond
(:comment drawing) (:draft local) (rx/of (dcm/close-thread))
(update state :workspace-drawing dissoc :comment) (:open local) (rx/of (dcm/close-thread))
:else (rx/of #(dissoc % :workspace-drawing)))))))
(:open local)
(update state :workspace-comments dissoc :open)
:else
(dissoc state :workspace-drawing))))))
;; Event responsible of the what should be executed when user clicked ;; Event responsible of the what should be executed when user clicked
;; on the comments layer. An option can be create a new draft thread, ;; on the comments layer. An option can be create a new draft thread,
@ -93,215 +62,32 @@
(defn- handle-comment-layer-click (defn- handle-comment-layer-click
[position] [position]
(ptk/reify ::handle-comment-layer-click (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 ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [file-id (get-in state [:workspace-file :id]) (let [local (:comments-local state)]
page-id (:current-page-id state) (if (some? (:open local))
params (assoc data (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 :page-id page-id
:file-id file-id)] :file-id file-id}]
(->> (rp/mutation :create-comment-thread params) (rx/of (dcm/create-draft 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)))))))
(defn center-to-comment-thread (defn center-to-comment-thread
[{:keys [id position] :as thread}] [{:keys [id position] :as thread}]
(us/assert ::comment-thread thread) (us/assert ::dcm/comment-thread thread)
(ptk/reify :center-to-comment-thread (ptk/reify :center-to-comment-thread
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(update state :workspace-local (update state :workspace-local
(fn [{:keys [vbox vport zoom] :as local}] (fn [{:keys [vbox vport zoom] :as local}]
;; (prn "position=" position)
;; (prn "vbox=" vbox)
;; (prn "vport=" vport)
(let [pw (/ 50 zoom) (let [pw (/ 50 zoom)
ph (/ 200 zoom) ph (/ 200 zoom)
nw (mth/round (- (/ (:width vbox) 2) pw)) nw (mth/round (- (/ (:width vbox) 2) pw))
nh (mth/round (- (/ (:height vbox) 2) ph)) nh (mth/round (- (/ (:height vbox) 2) ph))
nx (- (:x position) nw) nx (- (:x position) nw)
ny (- (:y position) nh)] ny (- (:y position) nh)]
(update local :vbox assoc :x nx :y ny)))) (update local :vbox assoc :x nx :y ny)))))))
)))

View file

@ -37,8 +37,6 @@
;; --- Persistence ;; --- Persistence
(defn initialize-file-persistence (defn initialize-file-persistence
[file-id] [file-id]
(ptk/reify ::initialize-persistence (ptk/reify ::initialize-persistence
@ -225,12 +223,6 @@
:else :else
(throw error)))))))) (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 (defn- bundle-fetched
[file users project libraries] [file users project libraries]
(ptk/reify ::bundle-fetched (ptk/reify ::bundle-fetched
@ -243,7 +235,7 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [users (map assoc-profile-avatar users)] (let [users (map avatars/assoc-profile-avatar users)]
(assoc state (assoc state
:workspace-undo {} :workspace-undo {}
:workspace-project project :workspace-project project

View file

@ -207,3 +207,7 @@
(def viewer-local (def viewer-local
(l/derived :viewer-local st/state)) (l/derived :viewer-local st/state))
(def comments-local
(l/derived :comments-local st/state))

View file

@ -31,11 +31,14 @@
(rx/throw {:type :authorization (rx/throw {:type :authorization
:code :not-authorized}) :code :not-authorized})
(= (:status response) 404)
(rx/throw (:body response))
(= 0 (:status response)) (= 0 (:status response))
(rx/throw {:type :offline}) (rx/throw {:type :offline})
:else :else
(rx/throw {:type :internal-error (rx/throw {:type :server-error
:status (:status response) :status (:status response)
:body (:body response)}))) :body (:body response)})))

View file

@ -13,6 +13,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.spec :as us]
[app.main.data.auth :refer [logout]] [app.main.data.auth :refer [logout]]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.refs :as refs] [app.main.refs :as refs]
@ -20,6 +21,7 @@
[app.main.ui.auth :refer [auth]] [app.main.ui.auth :refer [auth]]
[app.main.ui.auth.verify-token :refer [verify-token]] [app.main.ui.auth.verify-token :refer [verify-token]]
[app.main.ui.cursors :as c] [app.main.ui.cursors :as c]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard :refer [dashboard]] [app.main.ui.dashboard :refer [dashboard]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
@ -27,7 +29,7 @@
[app.main.ui.settings :as settings] [app.main.ui.settings :as settings]
[app.main.ui.static :refer [not-found-page not-authorized-page]] [app.main.ui.static :refer [not-found-page not-authorized-page]]
[app.main.ui.viewer :refer [viewer-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.main.ui.workspace :as workspace]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.timers :as ts] [app.util.timers :as ts]
@ -39,6 +41,19 @@
;; --- Routes ;; --- 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 (def routes
[["/auth" [["/auth"
["/login" :auth-login] ["/login" :auth-login]
@ -53,8 +68,17 @@
["/password" :settings-password] ["/password" :settings-password]
["/options" :settings-options]] ["/options" :settings-options]]
["/view/:file-id/:page-id" :viewer] ["/view/:file-id/:page-id"
["/handoff/:file-id/:page-id" :handoff] {: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-found" :not-found]
["/not-authorized" :not-authorized] ["/not-authorized" :not-authorized]
@ -86,6 +110,8 @@
(mf/defc app (mf/defc app
{::mf/wrap [#(mf/catch % {:fallback app-error})]} {::mf/wrap [#(mf/catch % {:fallback app-error})]}
[{:keys [route] :as props}] [{:keys [route] :as props}]
[:& (mf/provider ctx/current-route) {:value route}
(case (get-in route [:data :name]) (case (get-in route [:data :name])
(:auth-login (:auth-login
:auth-register :auth-register
@ -120,19 +146,21 @@
[:& dashboard {:route route}] [:& dashboard {:route route}]
:viewer :viewer
(let [index (d/parse-integer (get-in route [:params :query :index])) (let [index (get-in route [:query-params :index])
token (get-in route [:params :query :token]) token (get-in route [:query-params :token])
file-id (uuid (get-in route [:params :path :file-id])) section (get-in route [:query-params :section] :interactions)
page-id (uuid (get-in route [:params :path :page-id]))] file-id (get-in route [:path-params :file-id])
page-id (get-in route [:path-params :page-id])]
[:& viewer-page {:page-id page-id [:& viewer-page {:page-id page-id
:file-id file-id :file-id file-id
:section section
:index index :index index
:token token}]) :token token}])
:handoff :handoff
(let [index (d/parse-integer (get-in route [:params :query :index])) (let [file-id (get-in route [:path-params :file-id])
file-id (uuid (get-in route [:params :path :file-id])) page-id (get-in route [:path-params :page-id])
page-id (uuid (get-in route [:params :path :page-id]))] index (get-in route [:query-params :index])]
[:& handoff {:page-id page-id [:& handoff {:page-id page-id
:file-id file-id :file-id file-id
:index index}]) :index index}])
@ -163,7 +191,7 @@
:not-found :not-found
[:& not-found-page] [:& not-found-page]
nil)) nil)])
(mf/defc app-wrapper (mf/defc app-wrapper
[] []
@ -229,7 +257,7 @@
:type :error :type :error
:timeout 5000})))))) :timeout 5000}))))))
(defmethod ptk/handle-error :internal-error (defmethod ptk/handle-error :server-error
[{:keys [status] :as error}] [{:keys [status] :as error}]
(cond (cond
(= status 429) (= status 429)
@ -243,6 +271,13 @@
(st/emitf (dm/show {:content "Unable to connect to backend, wait a little bit and refresh." (st/emitf (dm/show {:content "Unable to connect to backend, wait a little bit and refresh."
:type :error}))))) :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 (defonce uncaught-error-handler
(letfn [(on-error [event] (letfn [(on-error [event]
(ptk/handle-error (unchecked-get event "error")) (ptk/handle-error (unchecked-get event "error"))

View file

@ -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)]]))

View file

@ -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]]))

View file

@ -14,6 +14,7 @@
(def embed-ctx (mf/create-context false)) (def embed-ctx (mf/create-context false))
(def render-ctx (mf/create-context nil)) (def render-ctx (mf/create-context nil))
(def current-route (mf/create-context nil))
(def current-team-id (mf/create-context nil)) (def current-team-id (mf/create-context nil))
(def current-project-id (mf/create-context nil)) (def current-project-id (mf/create-context nil))
(def current-page-id (mf/create-context nil)) (def current-page-id (mf/create-context nil))

View file

@ -7,28 +7,27 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff (ns app.main.ui.handoff
(:require (:require
[rumext.alpha :as mf]
[beicon.core :as rx]
[goog.events :as events]
[okulary.core :as l]
[app.common.exceptions :as ex] [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.data.viewer :as dv]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.viewer.header :refer [header]] [app.main.ui.viewer.header :refer [header]]
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
[app.main.ui.viewer.handoff.render :refer [render-frame-svg]] [app.util.dom :as dom]
[app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] [app.util.i18n :as i18n :refer [t tr]]
[app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]]) [beicon.core :as rx]
[goog.events :as events]
[okulary.core :as l]
[rumext.alpha :as mf])
(:import goog.events.EventType)) (:import goog.events.EventType))
(defn handle-select-frame [frame] (defn handle-select-frame [frame]
@ -37,7 +36,7 @@
(st/emit! (dv/select-shape (:id frame))))) (st/emit! (dv/select-shape (:id frame)))))
(mf/defc render-panel (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) (let [locale (mf/deref i18n/locale)
frames (:frames data []) frames (:frames data [])
objects (:objects data) objects (:objects data)
@ -65,26 +64,23 @@
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
[:div.handoff-svg-container [:div.handoff-svg-container
[:& render-frame-svg {:frame-id (:id frame) [:& render-frame-svg {:frame-id (:id frame)
:zoom (:zoom local) :zoom (:zoom state)
:objects objects}]]] :objects objects}]]]
[:& right-sidebar {:frame frame [:& right-sidebar {:frame frame
:page-id page-id :page-id page-id
:file-id file-id}]])])) :file-id file-id}]])]))
(mf/defc handoff-content (mf/defc handoff-content
[{:keys [data local index page-id file-id] :as props}] [{:keys [data state index page-id file-id] :as props}]
(let [on-mouse-wheel
(let [container (mf/use-ref) (mf/use-callback
[toggle-fullscreen fullscreen?] (hooks/use-fullscreen container)
on-mouse-wheel
(fn [event] (fn [event]
(when (kbd/ctrl? event) (when (kbd/ctrl? event)
(dom/prevent-default event) (dom/prevent-default event)
(let [event (.getBrowserEvent ^js event)] (let [event (.getBrowserEvent ^js event)]
(if (pos? (.-deltaY ^js event)) (if (pos? (.-deltaY ^js event))
(st/emit! dv/decrease-zoom) (st/emit! dv/decrease-zoom)
(st/emit! dv/increase-zoom))))) (st/emit! dv/increase-zoom))))))
on-mount on-mount
(fn [] (fn []
@ -98,37 +94,39 @@
(mf/use-effect on-mount) (mf/use-effect on-mount)
(hooks/use-shortcuts dv/shortcuts) (hooks/use-shortcuts dv/shortcuts)
[:div.handoff-layout {:class (classnames :fullscreen fullscreen?) [:& fs/fullscreen-wrapper {}
:ref container} [:div.handoff-layout
[:& header {:data data [:& header
:toggle-fullscreen toggle-fullscreen {:data data
:fullscreen? fullscreen? :state state
:local local
:index index :index index
:screen :handoff}] :section :handoff}]
[:div.viewer-content [:div.viewer-content
(when (:show-thumbnails local) (when (:show-thumbnails state)
[:& thumbnails-panel {:index index [:& thumbnails-panel {:index index
:data data :data data
:screen :handoff}]) :screen :handoff}])
[:& render-panel {:data data [:& render-panel {:data data
:local local :state state
:index index :index index
:page-id page-id :page-id page-id
:file-id file-id}]]])) :file-id file-id}]]]]))
(mf/defc handoff (mf/defc handoff
[{:keys [file-id page-id index] :as props}] [{:keys [file-id page-id index token] :as props}]
(mf/use-effect (mf/use-effect
(mf/deps file-id page-id) (mf/deps file-id page-id token)
(fn [] (fn []
(st/emit! (dv/initialize props)))) (st/emit! (dv/initialize props))))
(let [data (mf/deref refs/viewer-data) (let [data (mf/deref refs/viewer-data)
local (mf/deref refs/viewer-local)] state (mf/deref refs/viewer-local)]
(when data
[:& handoff-content {:file-id file-id (when (and data state)
[:& handoff-content
{:file-id file-id
:page-id page-id :page-id page-id
:index index :index index
:local local :state state
:data data}]))) :data data}])))

View file

@ -7,19 +7,19 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes (ns app.main.ui.handoff.attributes
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.util.i18n :as i18n] [app.util.i18n :as i18n]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.ui.viewer.handoff.exports :refer [exports]] [app.main.ui.handoff.exports :refer [exports]]
[app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]] [app.main.ui.handoff.attributes.layout :refer [layout-panel]]
[app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]] [app.main.ui.handoff.attributes.fill :refer [fill-panel]]
[app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]] [app.main.ui.handoff.attributes.stroke :refer [stroke-panel]]
[app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]] [app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
[app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]] [app.main.ui.handoff.attributes.blur :refer [blur-panel]]
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]] [app.main.ui.handoff.attributes.image :refer [image-panel]]
[app.main.ui.viewer.handoff.attributes.text :refer [text-panel]])) [app.main.ui.handoff.attributes.text :refer [text-panel]]))
(def type->options (def type->options
{:multiple [:fill :stroke :image :text :shadow :blur] {:multiple [:fill :stroke :image :text :shadow :blur]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.blur (ns app.main.ui.handoff.attributes.blur
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.common (ns app.main.ui.handoff.attributes.common
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.fill (ns app.main.ui.handoff.attributes.fill
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.util.i18n :refer [t]] [app.util.i18n :refer [t]]
@ -15,7 +15,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]] [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]) (def fill-attributes [:fill-color :fill-color-gradient])

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.image (ns app.main.ui.handoff.attributes.image
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.layout (ns app.main.ui.handoff.attributes.layout
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.shadow (ns app.main.ui.handoff.attributes.shadow
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -16,7 +16,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]] [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] (defn has-shadow? [shape]
(:shadow shape)) (:shadow shape))

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.stroke (ns app.main.ui.handoff.attributes.stroke
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -16,7 +16,7 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.code-gen :as cg] [app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]] [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] (defn shape->color [shape]
{:color (:stroke-color shape) {:color (:stroke-color shape)

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.attributes.text (ns app.main.ui.handoff.attributes.text
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -19,7 +19,7 @@
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.webapi :as wapi] [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.util.code-gen :as cg]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.copy-button :refer [copy-button]])) [app.main.ui.components.copy-button :refer [copy-button]]))

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.code (ns app.main.ui.handoff.code
(:require (:require
["js-beautify" :as beautify] ["js-beautify" :as beautify]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.exports (ns app.main.ui.handoff.exports
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[beicon.core :as rx] [beicon.core :as rx]

View file

@ -7,18 +7,18 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.left-sidebar (ns app.main.ui.handoff.left-sidebar
(:require (:require
[rumext.alpha :as mf]
[okulary.core :as l]
[app.common.data :as d] [app.common.data :as d]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.store :as st]
[app.util.dom :as dom]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [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 (def selected-shapes
(l/derived (comp :selected :viewer-local) st/state)) (l/derived (comp :selected :viewer-local) st/state))
@ -29,7 +29,7 @@
(defn- make-collapsed-iref (defn- make-collapsed-iref
[id] [id]
#(-> (l/in [:viewer-local :collapsed id]) #(-> (l/in [:viewer-local :collapsed id])
(l/derived st/state) )) (l/derived st/state)))
(mf/defc layer-item (mf/defc layer-item
[{:keys [index item selected objects disable-collapse?] :as props}] [{:keys [index item selected objects disable-collapse?] :as props}]

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; 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" "The main container for a frame in handoff mode"
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
@ -30,7 +30,7 @@
[app.main.ui.shapes.path :as path] [app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect] [app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.text :as text] [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]])) [app.main.ui.shapes.shape :refer [shape-container]]))
(declare shape-container-factory) (declare shape-container-factory)

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.right-sidebar (ns app.main.ui.handoff.right-sidebar
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[okulary.core :as l] [okulary.core :as l]
@ -16,8 +16,8 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.components.tab-container :refer [tab-container tab-element]] [app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.workspace.sidebar.layers :refer [element-icon]] [app.main.ui.workspace.sidebar.layers :refer [element-icon]]
[app.main.ui.viewer.handoff.attributes :refer [attributes]] [app.main.ui.handoff.attributes :refer [attributes]]
[app.main.ui.viewer.handoff.code :refer [code]])) [app.main.ui.handoff.code :refer [code]]))
(defn make-selected-shapes-iref (defn make-selected-shapes-iref
[] []

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.viewer.handoff.selection-feedback (ns app.main.ui.handoff.selection-feedback
(:require (:require
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]

View file

@ -50,23 +50,6 @@
(fn [] (mousetrap/reset)))) (fn [] (mousetrap/reset))))
nil) 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 (defn invisible-image
[] []
(let [img (js/Image.) (let [img (js/Image.)

View file

@ -9,31 +9,182 @@
(ns app.main.ui.viewer (ns app.main.ui.viewer
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [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.viewer :as dv]
[app.main.data.comments :as dcm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [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.hooks :as hooks]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.keyboard :as kbd]
[app.main.ui.viewer.header :refer [header]] [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.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.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [t tr]]
[beicon.core :as rx]
[goog.events :as events] [goog.events :as events]
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf]) [rumext.alpha :as mf]))
(:import goog.events.EventType))
(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 (mf/defc main-panel
[{:keys [data local index]}] [{:keys [data state index section]}]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
frames (:frames data []) frames (:frames data)
objects (:objects data)
frame (get frames index)] frame (get frames index)]
[:section.viewer-preview [:section.viewer-preview
(cond (cond
@ -45,22 +196,20 @@
[:section.empty-state [:section.empty-state
[:span (t locale "viewer.frame-not-found")]] [:span (t locale "viewer.frame-not-found")]]
:else (some? state)
[:& frame-svg {:frame frame [:& viewport
:show-interactions? (:show-interactions? local) {:data data
:zoom (:zoom local) :section section
:objects objects}])])) :index index
:state state
}])]))
(mf/defc viewer-content (mf/defc viewer-content
[{:keys [data local index] :as props}] [{:keys [data state index section] :as props}]
(let [container (mf/use-ref) (let [on-click
[toggle-fullscreen fullscreen?] (hooks/use-fullscreen container)
on-click
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(let [mode (get local :interactions-mode)] (let [mode (get state :interactions-mode)]
(when (= mode :show-on-click) (when (= mode :show-on-click)
(st/emit! dv/flash-interactions)))) (st/emit! dv/flash-interactions))))
@ -73,49 +222,63 @@
(st/emit! dv/decrease-zoom) (st/emit! dv/decrease-zoom)
(st/emit! dv/increase-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 on-mount
(fn [] (fn []
;; bind with passive=false to allow the event to be cancelled ;; bind with passive=false to allow the event to be cancelled
;; https://stackoverflow.com/a/57582286/3219895 ;; https://stackoverflow.com/a/57582286/3219895
(let [key1 (events/listen goog/global EventType.WHEEL (let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false})
on-mouse-wheel #js {"passive" false})] key2 (events/listen js/document "keydown" on-key-down)
key3 (events/listen js/document "click" on-click)]
(fn [] (fn []
(events/unlistenByKey key1))))] (events/unlistenByKey key1)
(events/unlistenByKey key2)
(events/unlistenByKey key3))))]
(mf/use-effect on-mount) (mf/use-effect on-mount)
(hooks/use-shortcuts dv/shortcuts) (hooks/use-shortcuts dv/shortcuts)
[:div.viewer-layout {:class (classnames :fullscreen fullscreen?) [:& fs/fullscreen-wrapper {}
:ref container} [: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} [:div.viewer-content {:on-click on-click}
(when (:show-thumbnails local) (when (:show-thumbnails state)
[:& thumbnails-panel {:screen :viewer [:& thumbnails-panel {:screen :viewer
:index index :index index
:data data}]) :data data}])
[:& main-panel {:data data [:& main-panel {:data data
:local local :section section
:index index}]]])) :state state
:index index}]]]]))
;; --- Component: Viewer Page ;; --- Component: Viewer Page
(mf/defc 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/use-effect
(mf/deps file-id page-id token) (mf/deps file-id page-id token)
(fn [] (st/emitf (dv/initialize props)))
(st/emit! (dv/initialize props))))
(let [data (mf/deref refs/viewer-data) (let [data (mf/deref refs/viewer-data)
local (mf/deref refs/viewer-local)] state (mf/deref refs/viewer-local)]
(when data (when (and data state)
[:& viewer-content {:index index [:& viewer-content
:local local {:index index
:section section
:state state
:data data}]))) :data data}])))

View file

@ -9,21 +9,23 @@
(ns app.main.ui.viewer.header (ns app.main.ui.viewer.header
(:require (:require
[rumext.alpha :as mf] [app.common.math :as mth]
[cuerdas.core :as str] [app.common.uuid :as uuid]
[app.main.ui.icons :as i]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.viewer :as dv] [app.main.data.viewer :as dv]
[app.main.data.comments :as dcm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [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.data :refer [classnames]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]] [app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt] [app.util.router :as rt]
[app.common.math :as mth] [app.util.webapi :as wapi]
[app.common.uuid :as uuid] [cuerdas.core :as str]
[app.util.webapi :as wapi])) [rumext.alpha :as mf]))
(mf/defc zoom-widget (mf/defc zoom-widget
{:wrap [mf/memo]} {:wrap [mf/memo]}
@ -40,7 +42,7 @@
[:span.dropdown-button i/arrow-down] [:span.dropdown-button i/arrow-down]
[:& dropdown {:show @show-dropdown? [:& dropdown {:show @show-dropdown?
:on-close #(reset! show-dropdown? false)} :on-close #(reset! show-dropdown? false)}
[:ul.zoom-dropdown [:ul.dropdown.zoom-dropdown
[:li {:on-click on-increase} [:li {:on-click on-increase}
"Zoom in" [:span "+"]] "Zoom in" [:span "+"]]
[:li {:on-click on-decrease} [:li {:on-click on-decrease}
@ -52,29 +54,6 @@
[:li {:on-click on-zoom-to-200} [:li {:on-click on-zoom-to-200}
"Zoom to 200%" [:span "Shift + 2"]]]]])) "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 (mf/defc share-link
[{:keys [page token] :as props}] [{:keys [page token] :as props}]
(let [show-dropdown? (mf/use-state false) (let [show-dropdown? (mf/use-state false)
@ -103,7 +82,7 @@
[:& dropdown {:show @show-dropdown? [:& dropdown {:show @show-dropdown?
:on-close #(swap! show-dropdown? not) :on-close #(swap! show-dropdown? not)
:container dropdown-ref} :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")] [:span.share-link-title (t locale "viewer.header.share.title")]
[:div.share-link-input [:div.share-link-input
(if (string? token) (if (string? token)
@ -121,16 +100,87 @@
[:button.btn-primary {:on-click create} [:button.btn-primary {:on-click create}
(t locale "viewer.header.share.create-link")])]]]])) (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 (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 (let [{:keys [project file page frames]} data
fullscreen (mf/use-ctx fs/fullscreen-context)
total (count frames) total (count frames)
on-click #(st/emit! dv/toggle-thumbnails-panel) locale (mf/deref i18n/locale)
interactions-mode (:interactions-mode local)
locale (i18n/use-locale)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
anonymous? (= uuid/zero (:id profile)) anonymous? (= uuid/zero (:id profile))
@ -138,17 +188,34 @@
file-id (get-in data [:file :id]) file-id (get-in data [:file :id])
page-id (get-in data [:page :id]) page-id (get-in data [:page :id])
on-edit #(st/emit! (rt/nav :workspace 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 {:project-id project-id
:file-id file-id} :file-id file-id}
{:page-id page-id})) {:page-id page-id})))
navigate
change-screen (mf/use-callback
(fn [screen] (fn [section]
(st/emit! (st/emit!
(rt/nav screen (case section
:interactions
(rt/nav :viewer
{:file-id file-id :page-id page-id} {:file-id file-id :page-id page-id}
{:index index})))] {: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})))))]
[:header.viewer-header [:header.viewer-header
[:div.main-icon [:div.main-icon
[:a {:on-click on-edit} i/logo-icon]] [:a {:on-click on-edit} i/logo-icon]]
@ -160,19 +227,33 @@
[:span.file-name (:name file)] [:span.file-name (:name file)]
[:span "/"] [:span "/"]
[:span.page-name (:name page)] [:span.page-name (:name page)]
[:span.dropdown-button i/arrow-down] [:span.show-thumbnails-button i/arrow-down]
[:span.counters (str (inc index) " / " total)]] [:span.counters (str (inc index) " / " total)]]
[:div.mode-zone [:div.mode-zone
[:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :viewer) [:button.mode-zone-button.tooltip.tooltip-bottom
(change-screen :viewer)) {:on-click #(navigate :interactions)
:class (when (= screen :viewer) "active") :alt "View mode"} i/play] :class (dom/classnames :active (= section :interactions))
[:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :handoff) :alt "View mode"}
(change-screen :handoff)) i/play]
:class (when (= screen :handoff) "active") :alt "Code mode"} i/code]]
[: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 [: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? (when-not anonymous?
[:& share-link {:token (:share-token data) [:& share-link {:token (:share-token data)
@ -183,17 +264,17 @@
(t locale "viewer.header.edit-page")]) (t locale "viewer.header.edit-page")])
[:& zoom-widget [:& zoom-widget
{:zoom (:zoom local) {:zoom (:zoom state)
:on-increase #(st/emit! dv/increase-zoom) :on-increase (st/emitf dv/increase-zoom)
:on-decrease #(st/emit! dv/decrease-zoom) :on-decrease (st/emitf dv/decrease-zoom)
:on-zoom-to-50 #(st/emit! dv/zoom-to-50) :on-zoom-to-50 (st/emitf dv/zoom-to-50)
:on-zoom-to-100 #(st/emit! dv/reset-zoom) :on-zoom-to-100 (st/emitf dv/reset-zoom)
:on-zoom-to-200 #(st/emit! dv/zoom-to-200)}] :on-zoom-to-200 (st/emitf dv/zoom-to-200)}]
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom [:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom
{:alt (t locale "viewer.header.fullscreen") {:alt (t locale "viewer.header.fullscreen")
:on-click toggle-fullscreen} :on-click #(if @fullscreen (fullscreen false) (fullscreen true))}
(if fullscreen? (if @fullscreen
i/full-screen-off i/full-screen-off
i/full-screen)] i/full-screen)]
]])) ]]))

View file

@ -111,8 +111,7 @@
on-item-click on-item-click
(fn [event index] (fn [event index]
(compare-and-set! selected false true) (compare-and-set! selected false true)
(st/emit! (rt/nav screen {:file-id file-id (st/emit! (dv/go-to-frame-by-index index))
:page-id page-id} {:index index}))
(when @expanded? (when @expanded?
(on-close)))] (on-close)))]
[:& dropdown' {:on-close on-close [:& dropdown' {:on-close on-close

View file

@ -12,362 +12,54 @@
[app.config :as cfg] [app.config :as cfg]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.comments :as dwcm] [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.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.components.dropdown :refer [dropdown]] [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.icons :as i]
[app.main.ui.keyboard :as kbd] [app.main.ui.comments :as cmt]
[app.main.ui.workspace.colorpicker]
[app.main.ui.workspace.context-menu :refer [context-menu]]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.timers :as tm] [app.util.timers :as tm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.object :as obj]
[beicon.core :as rx]
[app.util.i18n :as i18n :refer [t tr]] [app.util.i18n :as i18n :refer [t tr]]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.alpha :as mf])) [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 (def threads-ref
(l/derived :comment-threads st/state)) (l/derived :comment-threads st/state))
(def workspace-comments-ref
(l/derived :workspace-comments st/state))
(mf/defc comments-layer (mf/defc comments-layer
[{:keys [vbox vport zoom file-id page-id drawing] :as props}] [{:keys [vbox vport zoom file-id page-id drawing] :as props}]
(let [pos-x (* (- (:x vbox)) zoom) (let [pos-x (* (- (:x vbox)) zoom)
pos-y (* (- (:y vbox)) zoom) pos-y (* (- (:y vbox)) zoom)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
local (mf/deref workspace-comments-ref) local (mf/deref refs/comments-local)
threads-map (mf/deref threads-ref) threads-map (mf/deref threads-ref)
threads (->> (vals threads-map) threads (->> (vals threads-map)
(filter #(= (:page-id %) page-id)) (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/use-effect
(mf/deps file-id) (mf/deps file-id)
@ -377,22 +69,27 @@
(st/emit! ::dwcm/finalize)))) (st/emit! ::dwcm/finalize))))
[:div.workspace-comments [:div.workspace-comments
[:div.comments-layer
{:style {:width (str (:width vport) "px") {:style {:width (str (:width vport) "px")
:height (str (:height vport) "px")}} :height (str (:height vport) "px")}}
[:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} [:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}}
(for [item threads] (for [item threads]
[:& thread-bubble {:thread item [:& cmt/thread-bubble {:thread item
:zoom zoom :zoom zoom
:on-click on-bubble-click
:open? (= (:id item) (:open local)) :open? (= (:id item) (:open local))
:key (:seqn item)}]) :key (:seqn item)}])
(when-let [id (:open local)] (when-let [id (:open local)]
(when-let [thread (get threads-map id)] (when-let [thread (get threads-map id)]
[:& thread-comments {:thread thread [:& cmt/thread-comments {:thread thread
:zoom zoom}])) :zoom zoom}]))
(when-let [draft (:comment drawing)] (when-let [draft (:comment drawing)]
[:& draft-thread {:draft draft :zoom zoom}])]])) [:& 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)))) (st/emit! (dw/go-to-page (:page-id item))))
(tm/schedule (tm/schedule
(st/emitf (dwcm/center-to-comment-thread item) (st/emitf (dwcm/center-to-comment-thread item)
(dwcm/open-thread item)))))] (dcm/open-thread item)))))]
[:div.comment {:on-click on-click} [:div.comment {:on-click on-click}
[:div.author [:div.author
@ -461,41 +158,49 @@
(mf/defc sidebar-options (mf/defc sidebar-options
[{:keys [local] :as props}] [{:keys [local] :as props}]
(let [filter-yours (let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
(mf/use-callback locale (mf/deref i18n/locale)
(mf/deps local)
(st/emitf (dwcm/update-filters {:main :yours})))
filter-all update-mode
(mf/use-callback (mf/use-callback
(mf/deps local) (fn [mode]
(st/emitf (dwcm/update-filters {:main :all}))) (st/emit! (dcm/update-filters {:mode mode}))))
toggle-resolved update-show
(mf/use-callback (mf/use-callback
(mf/deps local) (fn [mode]
(st/emitf (dwcm/update-filters {:resolved (not (:filter-resolved local))})))] (st/emit! (dcm/update-filters {:show mode}))))]
[: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")]]
[:ul.dropdown.sidebar-options-dropdown
[:li {:on-click filter-all} "All"]
[:li {:on-click filter-yours} "Only yours"]
[:hr] [:hr]
(if (:filter-resolved local)
[:li {:on-click toggle-resolved} "Show resolved comments"] [:li {:class (dom/classnames :selected (= :pending cshow))
[:li {:on-click toggle-resolved} "Hide resolved comments"])])) :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 (mf/defc comments-sidebar
[] []
(let [threads-map (mf/deref threads-ref) (let [threads-map (mf/deref threads-ref)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
local (mf/deref workspace-comments-ref) local (mf/deref refs/comments-local)
options? (mf/use-state false) options? (mf/use-state false)
tgroups (->> (vals threads-map) tgroups (->> (vals threads-map)
(sort-by :modified-at) (sort-by :modified-at)
(reverse) (reverse)
(apply-filters local profile) (dcm/apply-filters local profile)
(group-threads-by-page))] (dcm/group-threads-by-page))]
[:div.workspace-comments.workspace-comments-sidebar [:div.workspace-comments.workspace-comments-sidebar
[:div.sidebar-title [:div.sidebar-title
@ -520,30 +225,3 @@
:key (:page-id tgroup)}]])])])) :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)))))

View file

@ -35,3 +35,9 @@
(.fillText context letters (/ size 2) (/ size 1.5)) (.fillText context letters (/ size 2) (/ size 1.5))
(.toDataURL canvas))) (.toDataURL canvas)))
(defn assoc-profile-avatar
[{:keys [photo fullname] :as profile}]
(cond-> profile
(or (nil? photo) (empty? photo))
(assoc :photo (generate {:name fullname}))))

View file

@ -9,12 +9,11 @@
(ns app.util.dom (ns app.util.dom
(:require (:require
[goog.dom :as dom] [app.common.exceptions :as ex]
[cuerdas.core :as str]
[beicon.core :as rx]
[cuerdas.core :as str]
[app.common.geom.point :as gpt] [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 ;; --- Deprecated methods
@ -111,11 +110,11 @@
(defn select-text! (defn select-text!
[node] [node]
(.select node)) (.select ^js node))
(defn ^boolean equals? (defn ^boolean equals?
[node-a node-b] [node-a node-b]
(.isEqualNode node-a node-b)) (.isEqualNode ^js node-a node-b))
(defn get-event-files (defn get-event-files
"Extract the files from event instance." "Extract the files from event instance."
@ -162,6 +161,12 @@
y (.-clientY event)] y (.-clientY event)]
(gpt/point x y))) (gpt/point x y)))
(defn get-offset-position
[event]
(let [x (.-offsetX event)
y (.-offsetY event)]
(gpt/point x y)))
(defn get-client-size (defn get-client-size
[node] [node]
{:width (.-clientWidth ^js node) {:width (.-clientWidth ^js node)
@ -188,7 +193,16 @@
(defn fullscreen? (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? (defn ^boolean blob?
[v] [v]

View file

@ -76,3 +76,7 @@
(defn clj->props (defn clj->props
[props] [props]
(clj->js props :keyword-fn props-key-fn)) (clj->js props :keyword-fn props-key-fn))
(defn ^boolean in?
[obj prop]
(js* "~{} in ~{}" prop obj))

View file

@ -10,6 +10,8 @@
(ns app.util.webapi (ns app.util.webapi
"HTML5 web api helpers." "HTML5 web api helpers."
(:require (:require
[app.common.exceptions :as ex]
[app.util.object :as obj]
[promesa.core :as p] [promesa.core :as p]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -97,8 +99,26 @@
(defn request-fullscreen (defn request-fullscreen
[el] [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 (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.")))