mirror of
https://github.com/penpot/penpot.git
synced 2025-05-12 17:16:39 +02:00
🎉 Add comments to viewer.
This commit is contained in:
parent
e1db6d3a37
commit
64a6ba1949
45 changed files with 1629 additions and 1074 deletions
|
@ -203,6 +203,8 @@
|
|||
(defn retrieve-file-users
|
||||
[conn id]
|
||||
(->> (db/exec! conn [sql:file-users id id])
|
||||
;; TODO: seems like the frontend is no longer uses :photo-uri,
|
||||
;; so this can be removed probably.
|
||||
(mapv #(media/resolve-media-uris % [:photo :photo-uri]))))
|
||||
|
||||
(s/def ::file-users
|
||||
|
|
|
@ -50,8 +50,11 @@
|
|||
file (merge (dissoc file :data)
|
||||
(select-keys (:data file) [:colors :media :typographies]))
|
||||
libs (files/retrieve-file-libraries conn false file-id)
|
||||
users (files/retrieve-file-users conn file-id)
|
||||
|
||||
bundle {:file file
|
||||
:page page
|
||||
:users users
|
||||
:project project
|
||||
:libraries libs}]
|
||||
(if (string? token)
|
||||
|
|
|
@ -119,6 +119,7 @@
|
|||
(s/def ::url string?)
|
||||
(s/def ::fn fn?)
|
||||
(s/def ::point gpt/point?)
|
||||
(s/def ::id ::uuid)
|
||||
|
||||
;; --- Macros
|
||||
|
||||
|
|
|
@ -1608,28 +1608,19 @@
|
|||
"modals.delete-comment-thread.accept" : {
|
||||
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:236" ],
|
||||
"translations" : {
|
||||
"en" : null,
|
||||
"fr" : null,
|
||||
"ru" : null,
|
||||
"es" : null
|
||||
"en" : "Delete conversation"
|
||||
}
|
||||
},
|
||||
"modals.delete-comment-thread.message" : {
|
||||
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:235" ],
|
||||
"translations" : {
|
||||
"en" : null,
|
||||
"fr" : null,
|
||||
"ru" : null,
|
||||
"es" : null
|
||||
"en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted."
|
||||
}
|
||||
},
|
||||
"modals.delete-comment-thread.title" : {
|
||||
"used-in" : [ "src/app/main/ui/workspace/comments.cljs:234" ],
|
||||
"translations" : {
|
||||
"en" : null,
|
||||
"fr" : null,
|
||||
"ru" : null,
|
||||
"es" : null
|
||||
"en" : "Delete conversation"
|
||||
}
|
||||
},
|
||||
"modals.delete-file-confirm.accept" : {
|
||||
|
@ -1881,6 +1872,10 @@
|
|||
"es" : "No se encuentra el tablero."
|
||||
}
|
||||
},
|
||||
"labels.show-all-comments": "Show all comments",
|
||||
"labels.show-your-comments": "Show only yours comments",
|
||||
"labels.hide-resolved-comments": "Hide resolved comments",
|
||||
|
||||
"viewer.header.dont-show-interactions" : {
|
||||
"used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ],
|
||||
"translations" : {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
border-color: $color-gray-10;
|
||||
}
|
||||
|
||||
li {
|
||||
> li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $color-gray-60;
|
||||
|
@ -20,6 +20,12 @@
|
|||
height: 40px;
|
||||
padding: 5px 16px;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-20;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&.title {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
|
@ -29,4 +35,27 @@
|
|||
background-color: $color-primary-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.with-check {
|
||||
> li {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
> li:not(.selected) {
|
||||
svg { display: none; }
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-gray-50;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,15 +42,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
.view-options {
|
||||
.icon {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
&:hover {
|
||||
> svg {
|
||||
fill: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-gray-30;
|
||||
|
@ -58,11 +62,10 @@
|
|||
width: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: $color-primary;
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,13 +95,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
svg {
|
||||
.show-thumbnails-button svg {
|
||||
fill: $color-white;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-name {
|
||||
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 {
|
||||
left : 116px;
|
||||
top: 45px;
|
||||
left: 180px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.users-zone {
|
||||
|
|
|
@ -7,19 +7,18 @@
|
|||
}
|
||||
|
||||
.viewer-preview {
|
||||
height: 100vh;
|
||||
height: calc(100vh - 40px);
|
||||
|
||||
grid-row: 1 / span 2;
|
||||
grid-column: 1 / span 1;
|
||||
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: wrap;
|
||||
|
||||
|
||||
.empty-state {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -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%;
|
||||
height: 100%;
|
||||
grid-column: 1/span 2;
|
||||
|
@ -8,7 +20,10 @@
|
|||
overflow: hidden;
|
||||
|
||||
.threads {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-bubble {
|
||||
|
@ -104,8 +119,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.comment-container {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -132,7 +145,7 @@
|
|||
font-size: $fs13;
|
||||
|
||||
@include text-ellipsis;
|
||||
width: 110px;
|
||||
width: 150px;
|
||||
|
||||
}
|
||||
.timeago {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
(ns app.main
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.auth :refer [logout]]
|
||||
[app.main.data.users :as udu]
|
||||
[app.main.store :as st]
|
||||
|
@ -35,12 +36,26 @@
|
|||
|
||||
(declare reinit)
|
||||
|
||||
(s/def ::any any?)
|
||||
|
||||
(defn match-path
|
||||
[router path]
|
||||
(when-let [match (rt/match router path)]
|
||||
(if-let [conform (get-in match [:data :conform])]
|
||||
(let [spath (get conform :path-params ::any)
|
||||
squery (get conform :query-params ::any)]
|
||||
(-> (dissoc match :params)
|
||||
(assoc :path-params (us/conform spath (get match :path-params))
|
||||
:query-params (us/conform squery (get match :query-params)))))
|
||||
match)))
|
||||
|
||||
(defn on-navigate
|
||||
[router path]
|
||||
(let [match (rt/match router path)
|
||||
(let [match (match-path router path)
|
||||
profile (:profile storage)
|
||||
authed? (and (not (nil? profile))
|
||||
(not= (:id profile) uuid/zero))]
|
||||
|
||||
(cond
|
||||
(and (or (= path "")
|
||||
(nil? match))
|
||||
|
|
300
frontend/src/app/main/data/comments.cljs
Normal file
300
frontend/src/app/main/data/comments.cljs
Normal 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))))))
|
|
@ -23,7 +23,7 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[app.common.pages-helpers :as cph]))
|
||||
|
||||
;; --- Specs
|
||||
;; --- General Specs
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
|
@ -32,40 +32,63 @@
|
|||
(s/def ::file (s/keys :req-un [::id ::name]))
|
||||
(s/def ::page ::cp/page)
|
||||
|
||||
(s/def ::interactions-mode #{:hide :show :show-on-click})
|
||||
|
||||
(s/def ::bundle
|
||||
(s/keys :req-un [::project ::file ::page]))
|
||||
|
||||
|
||||
;; --- Initialization
|
||||
;; --- Local State Initialization
|
||||
|
||||
(def ^:private
|
||||
default-local-state
|
||||
{:zoom 1
|
||||
:interactions-mode :hide
|
||||
:interactions-show? false
|
||||
:comments-mode :all
|
||||
:comments-show :unresolved
|
||||
:selected #{}
|
||||
:collapsed #{}
|
||||
:hover nil})
|
||||
|
||||
(declare fetch-comment-threads)
|
||||
(declare fetch-bundle)
|
||||
(declare bundle-fetched)
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/string))
|
||||
(s/def ::section ::us/string)
|
||||
|
||||
(s/def ::initialize-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-in [::token]))
|
||||
|
||||
(defn initialize
|
||||
[{:keys [page-id file-id] :as params}]
|
||||
[{:keys [page-id file-id token] :as params}]
|
||||
(us/assert ::initialize-params params)
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :viewer-local {:zoom 1
|
||||
:page-id page-id
|
||||
:file-id file-id
|
||||
:interactions-mode :hide
|
||||
:show-interactions? false
|
||||
|
||||
:selected #{}
|
||||
:collapsed #{}
|
||||
:hover nil}))
|
||||
(update state :viewer-local
|
||||
(fn [lstate]
|
||||
(if (nil? lstate)
|
||||
default-local-state
|
||||
lstate))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/of (fetch-bundle params)))))
|
||||
(rx/of (fetch-bundle params)
|
||||
(fetch-comment-threads params)))))
|
||||
|
||||
;; --- Data Fetching
|
||||
|
||||
(s/def ::fetch-bundle-params
|
||||
(s/keys :req-un [::page-id ::file-id]
|
||||
:opt-in [::token]))
|
||||
|
||||
(defn fetch-bundle
|
||||
[{:keys [page-id file-id token]}]
|
||||
[{:keys [page-id file-id token] :as params}]
|
||||
(us/assert ::fetch-bundle-params params)
|
||||
(ptk/reify ::fetch-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
|
@ -76,6 +99,40 @@
|
|||
(rx/first)
|
||||
(rx/map bundle-fetched))))))
|
||||
|
||||
(defn fetch-comment-threads
|
||||
[{:keys [file-id page-id] :as params}]
|
||||
(letfn [(fetched [data state]
|
||||
(->> data
|
||||
(filter #(= page-id (:page-id %)))
|
||||
(d/index-by :id)
|
||||
(assoc state :comment-threads)))]
|
||||
(ptk/reify ::fetch-comment-threads
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comment-threads {:file-id file-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn refresh-comment-thread
|
||||
[{:keys [id file-id] :as thread}]
|
||||
(letfn [(fetched [thread state]
|
||||
(assoc-in state [:comment-threads id] thread))]
|
||||
(ptk/reify ::refresh-comment-thread
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comment-thread {:file-id file-id :id id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn fetch-comments
|
||||
[{:keys [thread-id]}]
|
||||
(us/assert ::us/uuid thread-id)
|
||||
(letfn [(fetched [comments state]
|
||||
(update state :comments assoc thread-id (d/index-by :id comments)))]
|
||||
(ptk/reify ::retrieve-comments
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn- extract-frames
|
||||
[objects]
|
||||
(let [root (get objects uuid/zero)]
|
||||
|
@ -86,7 +143,7 @@
|
|||
(vec))))
|
||||
|
||||
(defn bundle-fetched
|
||||
[{:keys [project file page share-token token libraries] :as bundle}]
|
||||
[{:keys [project file page share-token token libraries users] :as bundle}]
|
||||
(us/verify ::bundle bundle)
|
||||
(ptk/reify ::file-fetched
|
||||
ptk/UpdateEvent
|
||||
|
@ -94,9 +151,10 @@
|
|||
(let [objects (:objects page)
|
||||
frames (extract-frames objects)]
|
||||
(-> state
|
||||
(assoc :viewer-libraries (into {} (map #(vector (:id %) %) libraries))
|
||||
(assoc :viewer-libraries (d/index-by :id libraries)
|
||||
:viewer-data {:project project
|
||||
:objects objects
|
||||
:users (d/index-by :id users)
|
||||
:file file
|
||||
:page page
|
||||
:frames frames
|
||||
|
@ -178,9 +236,9 @@
|
|||
(watch [_ state stream]
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (get-in route [:params :query])
|
||||
pparams (get-in route [:params :path])
|
||||
index (d/parse-integer (:index qparams))]
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
index (:index qparams)]
|
||||
(when (pos? index)
|
||||
(rx/of (rt/nav screen pparams (assoc qparams :index (dec index)))))))))
|
||||
|
||||
|
@ -190,13 +248,15 @@
|
|||
(watch [_ state stream]
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (get-in route [:params :query])
|
||||
pparams (get-in route [:params :path])
|
||||
index (d/parse-integer (:index qparams))
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)
|
||||
index (:index qparams)
|
||||
total (count (get-in state [:viewer-data :frames]))]
|
||||
(when (< index (dec total))
|
||||
(rx/of (rt/nav screen pparams (assoc qparams :index (inc index)))))))))
|
||||
|
||||
(s/def ::interactions-mode #{:hide :show :show-on-click})
|
||||
|
||||
(defn set-interactions-mode
|
||||
[mode]
|
||||
(us/verify ::interactions-mode mode)
|
||||
|
@ -205,7 +265,7 @@
|
|||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:viewer-local :interactions-mode] mode)
|
||||
(assoc-in [:viewer-local :show-interactions?] (case mode
|
||||
(assoc-in [:viewer-local :interactions-show?] (case mode
|
||||
:hide false
|
||||
:show true
|
||||
:show-on-click false))))))
|
||||
|
@ -216,7 +276,7 @@
|
|||
(ptk/reify ::flash-interactions
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :show-interactions?] true))
|
||||
(assoc-in state [:viewer-local :interactions-show?] true))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
|
@ -229,26 +289,30 @@
|
|||
(ptk/reify ::flash-done
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:viewer-local :show-interactions?] false))))
|
||||
(assoc-in state [:viewer-local :interactions-show?] false))))
|
||||
|
||||
;; --- Navigation
|
||||
|
||||
(defn go-to-frame-by-index
|
||||
[index]
|
||||
(ptk/reify ::go-to-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [route (:route state)
|
||||
screen (-> route :data :name keyword)
|
||||
qparams (:query-params route)
|
||||
pparams (:path-params route)]
|
||||
(rx/of (rt/nav screen pparams (assoc qparams :index index)))))))
|
||||
|
||||
(defn go-to-frame
|
||||
[frame-id]
|
||||
(us/verify ::us/uuid frame-id)
|
||||
(ptk/reify ::go-to-frame
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (get-in state [:viewer-local :page-id])
|
||||
file-id (get-in state [:viewer-local :file-id])
|
||||
frames (get-in state [:viewer-data :frames])
|
||||
token (get-in state [:viewer-data :token])
|
||||
(let [frames (get-in state [:viewer-data :frames])
|
||||
index (d/index-of-pred frames #(= (:id %) frame-id))]
|
||||
(rx/of (rt/nav :viewer
|
||||
{:page-id page-id
|
||||
:file-id file-id}
|
||||
{:token token
|
||||
:index index}))))))
|
||||
(rx/of (go-to-frame-by-index index))))))
|
||||
|
||||
(defn set-current-frame [frame-id]
|
||||
(ptk/reify ::current-frame
|
||||
|
|
|
@ -144,6 +144,8 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state
|
||||
:current-file-id file-id
|
||||
:current-project-id project-id
|
||||
:workspace-presence {}))
|
||||
|
||||
ptk/WatchEvent
|
||||
|
|
|
@ -9,59 +9,34 @@
|
|||
|
||||
(ns app.main.data.workspace.comments
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages-helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.main.constants :as c]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[app.util.transit :as t]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(s/def ::comment-thread any?)
|
||||
(s/def ::comment any?)
|
||||
|
||||
(declare create-draft-thread)
|
||||
(declare retrieve-comment-threads)
|
||||
(declare refresh-comment-thread)
|
||||
(declare handle-interrupt)
|
||||
(declare handle-comment-layer-click)
|
||||
|
||||
(defn initialize-comments
|
||||
[file-id]
|
||||
(us/assert ::us/uuid file-id)
|
||||
(ptk/reify ::start-commenting
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local assoc :commenting true))
|
||||
|
||||
(ptk/reify ::initialize-comments
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper (rx/filter #(= ::finalize %) stream)]
|
||||
(rx/merge
|
||||
(rx/of (retrieve-comment-threads file-id))
|
||||
(rx/of (dcm/retrieve-comment-threads file-id))
|
||||
(->> stream
|
||||
(rx/filter ms/mouse-click?)
|
||||
(rx/switch-map #(rx/take 1 ms/mouse-position))
|
||||
(rx/mapcat #(rx/take 1 ms/mouse-position))
|
||||
(rx/map handle-comment-layer-click)
|
||||
(rx/take-until stoper))
|
||||
(->> stream
|
||||
|
@ -72,19 +47,13 @@
|
|||
(defn- handle-interrupt
|
||||
[]
|
||||
(ptk/reify ::handle-interrupt
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [local (:workspace-comments state)
|
||||
drawing (:workspace-drawing state)]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [local (:comments-local state)]
|
||||
(cond
|
||||
(:comment drawing)
|
||||
(update state :workspace-drawing dissoc :comment)
|
||||
|
||||
(:open local)
|
||||
(update state :workspace-comments dissoc :open)
|
||||
|
||||
:else
|
||||
(dissoc state :workspace-drawing))))))
|
||||
(:draft local) (rx/of (dcm/close-thread))
|
||||
(:open local) (rx/of (dcm/close-thread))
|
||||
:else (rx/of #(dissoc % :workspace-drawing)))))))
|
||||
|
||||
;; Event responsible of the what should be executed when user clicked
|
||||
;; on the comments layer. An option can be create a new draft thread,
|
||||
|
@ -93,215 +62,32 @@
|
|||
(defn- handle-comment-layer-click
|
||||
[position]
|
||||
(ptk/reify ::handle-comment-layer-click
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [local (:workspace-comments state)]
|
||||
(if (:open local)
|
||||
(update state :workspace-comments dissoc :open)
|
||||
(update state :workspace-drawing assoc
|
||||
:comment {:position position :content ""}))))))
|
||||
|
||||
(defn create-thread
|
||||
[data]
|
||||
(letfn [(created [{:keys [id comment] :as thread} state]
|
||||
(-> state
|
||||
(update :comment-threads assoc id (dissoc thread :comment))
|
||||
(update :workspace-comments assoc :open id)
|
||||
(update :workspace-drawing dissoc :comment)
|
||||
(update-in [:comments id] assoc (:id comment) comment)))]
|
||||
|
||||
(ptk/reify ::create-thread
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [file-id (get-in state [:workspace-file :id])
|
||||
page-id (:current-page-id state)
|
||||
params (assoc data
|
||||
(let [local (:comments-local state)]
|
||||
(if (some? (:open local))
|
||||
(rx/of (dcm/close-thread))
|
||||
(let [page-id (:current-page-id state)
|
||||
file-id (:current-file-id state)
|
||||
params {:position position
|
||||
:page-id page-id
|
||||
:file-id file-id)]
|
||||
(->> (rp/mutation :create-comment-thread params)
|
||||
(rx/map #(partial created %))))))))
|
||||
|
||||
(defn update-comment-thread-status
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::update-comment-thread-status
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comment-threads id] assoc :count-unread-comments 0))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :update-comment-thread-status {:id id})
|
||||
(rx/ignore)))))
|
||||
|
||||
|
||||
(defn update-comment-thread
|
||||
[{:keys [id is-resolved] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::update-comment-thread
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comment-threads id] assoc :is-resolved is-resolved))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :update-comment-thread {:id id :is-resolved is-resolved})
|
||||
(rx/ignore)))))
|
||||
|
||||
|
||||
(defn add-comment
|
||||
[thread content]
|
||||
(us/assert ::comment-thread thread)
|
||||
(us/assert ::us/string content)
|
||||
(letfn [(created [comment state]
|
||||
(update-in state [:comments (:id thread)] assoc (:id comment) comment))]
|
||||
(ptk/reify ::create-comment
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(rx/concat
|
||||
(->> (rp/mutation :add-comment {:thread-id (:id thread) :content content})
|
||||
(rx/map #(partial created %)))
|
||||
(rx/of (refresh-comment-thread thread)))))))
|
||||
|
||||
(defn update-comment
|
||||
[{:keys [id content thread-id] :as comment}]
|
||||
(us/assert ::comment comment)
|
||||
(ptk/reify :update-comment
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comments thread-id id] assoc :content content))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :update-comment {:id id :content content})
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn delete-comment-thread
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify :delete-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :comments dissoc id)
|
||||
(update :comment-threads dissoc id)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :delete-comment-thread {:id id})
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn delete-comment
|
||||
[{:keys [id thread-id] :as comment}]
|
||||
(us/assert ::comment comment)
|
||||
(ptk/reify :delete-comment
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/update-in-when state [:comments thread-id] dissoc id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation :delete-comment {:id id})
|
||||
(rx/ignore)))))
|
||||
|
||||
(defn refresh-comment-thread
|
||||
[{:keys [id file-id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(letfn [(fetched [thread state]
|
||||
(assoc-in state [:comment-threads id] thread))]
|
||||
(ptk/reify ::refresh-comment-thread
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comment-thread {:file-id file-id :id id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn retrieve-comment-threads
|
||||
[file-id]
|
||||
(us/assert ::us/uuid file-id)
|
||||
(letfn [(fetched [data state]
|
||||
(assoc state :comment-threads (d/index-by :id data)))]
|
||||
(ptk/reify ::retrieve-comment-threads
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comment-threads {:file-id file-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
(defn retrieve-comments
|
||||
[thread-id]
|
||||
(us/assert ::us/uuid thread-id)
|
||||
(letfn [(fetched [comments state]
|
||||
(update state :comments assoc thread-id (d/index-by :id comments)))]
|
||||
(ptk/reify ::retrieve-comments
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :comments {:thread-id thread-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Workspace (local) events
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn open-thread
|
||||
[{:keys [id] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(ptk/reify ::open-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-comments assoc :open id)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
|
||||
(defn close-thread
|
||||
[]
|
||||
(ptk/reify ::open-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-comments dissoc :open)
|
||||
(update :workspace-drawing dissoc :comment)))))
|
||||
|
||||
(defn update-draft-thread
|
||||
[data]
|
||||
(ptk/reify ::update-draft-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-drawing assoc :comment data))))
|
||||
|
||||
(defn update-filters
|
||||
[{:keys [main resolved]}]
|
||||
(ptk/reify ::update-filters
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-comments
|
||||
(fn [local]
|
||||
(cond-> local
|
||||
(some? main)
|
||||
(assoc :filter main)
|
||||
|
||||
(some? resolved)
|
||||
(assoc :filter-resolved resolved)))))))
|
||||
|
||||
:file-id file-id}]
|
||||
(rx/of (dcm/create-draft params))))))))
|
||||
|
||||
(defn center-to-comment-thread
|
||||
[{:keys [id position] :as thread}]
|
||||
(us/assert ::comment-thread thread)
|
||||
(us/assert ::dcm/comment-thread thread)
|
||||
(ptk/reify :center-to-comment-thread
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vbox vport zoom] :as local}]
|
||||
;; (prn "position=" position)
|
||||
;; (prn "vbox=" vbox)
|
||||
;; (prn "vport=" vport)
|
||||
(let [pw (/ 50 zoom)
|
||||
ph (/ 200 zoom)
|
||||
nw (mth/round (- (/ (:width vbox) 2) pw))
|
||||
nh (mth/round (- (/ (:height vbox) 2) ph))
|
||||
nx (- (:x position) nw)
|
||||
ny (- (:y position) nh)]
|
||||
(update local :vbox assoc :x nx :y ny))))
|
||||
|
||||
)))
|
||||
(update local :vbox assoc :x nx :y ny)))))))
|
||||
|
||||
|
||||
|
|
|
@ -37,8 +37,6 @@
|
|||
|
||||
;; --- Persistence
|
||||
|
||||
|
||||
|
||||
(defn initialize-file-persistence
|
||||
[file-id]
|
||||
(ptk/reify ::initialize-persistence
|
||||
|
@ -225,12 +223,6 @@
|
|||
:else
|
||||
(throw error))))))))
|
||||
|
||||
(defn assoc-profile-avatar
|
||||
[{:keys [photo fullname] :as profile}]
|
||||
(cond-> profile
|
||||
(or (nil? photo) (empty? photo))
|
||||
(assoc :photo (avatars/generate {:name fullname}))))
|
||||
|
||||
(defn- bundle-fetched
|
||||
[file users project libraries]
|
||||
(ptk/reify ::bundle-fetched
|
||||
|
@ -243,7 +235,7 @@
|
|||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [users (map assoc-profile-avatar users)]
|
||||
(let [users (map avatars/assoc-profile-avatar users)]
|
||||
(assoc state
|
||||
:workspace-undo {}
|
||||
:workspace-project project
|
||||
|
|
|
@ -207,3 +207,7 @@
|
|||
|
||||
(def viewer-local
|
||||
(l/derived :viewer-local st/state))
|
||||
|
||||
(def comments-local
|
||||
(l/derived :comments-local st/state))
|
||||
|
||||
|
|
|
@ -31,11 +31,14 @@
|
|||
(rx/throw {:type :authorization
|
||||
:code :not-authorized})
|
||||
|
||||
(= (:status response) 404)
|
||||
(rx/throw (:body response))
|
||||
|
||||
(= 0 (:status response))
|
||||
(rx/throw {:type :offline})
|
||||
|
||||
:else
|
||||
(rx/throw {:type :internal-error
|
||||
(rx/throw {:type :server-error
|
||||
:status (:status response)
|
||||
:body (:body response)})))
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.auth :refer [logout]]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -20,6 +21,7 @@
|
|||
[app.main.ui.auth :refer [auth]]
|
||||
[app.main.ui.auth.verify-token :refer [verify-token]]
|
||||
[app.main.ui.cursors :as c]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.dashboard :refer [dashboard]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.messages :as msgs]
|
||||
|
@ -27,7 +29,7 @@
|
|||
[app.main.ui.settings :as settings]
|
||||
[app.main.ui.static :refer [not-found-page not-authorized-page]]
|
||||
[app.main.ui.viewer :refer [viewer-page]]
|
||||
[app.main.ui.viewer.handoff :refer [handoff]]
|
||||
[app.main.ui.handoff :refer [handoff]]
|
||||
[app.main.ui.workspace :as workspace]
|
||||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.timers :as ts]
|
||||
|
@ -39,6 +41,19 @@
|
|||
|
||||
;; --- Routes
|
||||
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::viewer-path-params
|
||||
(s/keys :req-un [::file-id ::page-id]))
|
||||
|
||||
(s/def ::section ::us/keyword)
|
||||
(s/def ::index ::us/integer)
|
||||
(s/def ::token (s/nilable ::us/string))
|
||||
|
||||
(s/def ::viewer-query-params
|
||||
(s/keys :req-un [::index]
|
||||
:opt-un [::token ::section]))
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
|
@ -53,8 +68,17 @@
|
|||
["/password" :settings-password]
|
||||
["/options" :settings-options]]
|
||||
|
||||
["/view/:file-id/:page-id" :viewer]
|
||||
["/handoff/:file-id/:page-id" :handoff]
|
||||
["/view/:file-id/:page-id"
|
||||
{:name :viewer
|
||||
:conform
|
||||
{:path-params ::viewer-path-params
|
||||
:query-params ::viewer-query-params}}]
|
||||
|
||||
["/handoff/:file-id/:page-id"
|
||||
{:name :handoff
|
||||
:conform {:path-params ::viewer-path-params
|
||||
:query-params ::viewer-query-params}}]
|
||||
|
||||
["/not-found" :not-found]
|
||||
["/not-authorized" :not-authorized]
|
||||
|
||||
|
@ -86,6 +110,8 @@
|
|||
(mf/defc app
|
||||
{::mf/wrap [#(mf/catch % {:fallback app-error})]}
|
||||
[{:keys [route] :as props}]
|
||||
|
||||
[:& (mf/provider ctx/current-route) {:value route}
|
||||
(case (get-in route [:data :name])
|
||||
(:auth-login
|
||||
:auth-register
|
||||
|
@ -120,19 +146,21 @@
|
|||
[:& dashboard {:route route}]
|
||||
|
||||
:viewer
|
||||
(let [index (d/parse-integer (get-in route [:params :query :index]))
|
||||
token (get-in route [:params :query :token])
|
||||
file-id (uuid (get-in route [:params :path :file-id]))
|
||||
page-id (uuid (get-in route [:params :path :page-id]))]
|
||||
(let [index (get-in route [:query-params :index])
|
||||
token (get-in route [:query-params :token])
|
||||
section (get-in route [:query-params :section] :interactions)
|
||||
file-id (get-in route [:path-params :file-id])
|
||||
page-id (get-in route [:path-params :page-id])]
|
||||
[:& viewer-page {:page-id page-id
|
||||
:file-id file-id
|
||||
:section section
|
||||
:index index
|
||||
:token token}])
|
||||
|
||||
:handoff
|
||||
(let [index (d/parse-integer (get-in route [:params :query :index]))
|
||||
file-id (uuid (get-in route [:params :path :file-id]))
|
||||
page-id (uuid (get-in route [:params :path :page-id]))]
|
||||
(let [file-id (get-in route [:path-params :file-id])
|
||||
page-id (get-in route [:path-params :page-id])
|
||||
index (get-in route [:query-params :index])]
|
||||
[:& handoff {:page-id page-id
|
||||
:file-id file-id
|
||||
:index index}])
|
||||
|
@ -163,7 +191,7 @@
|
|||
:not-found
|
||||
[:& not-found-page]
|
||||
|
||||
nil))
|
||||
nil)])
|
||||
|
||||
(mf/defc app-wrapper
|
||||
[]
|
||||
|
@ -229,7 +257,7 @@
|
|||
:type :error
|
||||
:timeout 5000}))))))
|
||||
|
||||
(defmethod ptk/handle-error :internal-error
|
||||
(defmethod ptk/handle-error :server-error
|
||||
[{:keys [status] :as error}]
|
||||
(cond
|
||||
(= status 429)
|
||||
|
@ -243,6 +271,13 @@
|
|||
(st/emitf (dm/show {:content "Unable to connect to backend, wait a little bit and refresh."
|
||||
:type :error})))))
|
||||
|
||||
|
||||
(defmethod ptk/handle-error :not-found
|
||||
[{:keys [status] :as error}]
|
||||
(ts/schedule
|
||||
(st/emitf (dm/show {:content "Resource not found."
|
||||
:type :warning}))))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(on-error [event]
|
||||
(ptk/handle-error (unchecked-get event "error"))
|
||||
|
|
340
frontend/src/app/main/ui/comments.cljs
Normal file
340
frontend/src/app/main/ui/comments.cljs
Normal 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)]]))
|
57
frontend/src/app/main/ui/components/fullscreen.cljs
Normal file
57
frontend/src/app/main/ui/components/fullscreen.cljs
Normal 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]]))
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
(def embed-ctx (mf/create-context false))
|
||||
(def render-ctx (mf/create-context nil))
|
||||
|
||||
(def current-route (mf/create-context nil))
|
||||
(def current-team-id (mf/create-context nil))
|
||||
(def current-project-id (mf/create-context nil))
|
||||
(def current-page-id (mf/create-context nil))
|
||||
|
|
|
@ -7,28 +7,27 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff
|
||||
(ns app.main.ui.handoff
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.fullscreen :as fs]
|
||||
[app.main.ui.handoff.left-sidebar :refer [left-sidebar]]
|
||||
[app.main.ui.handoff.render :refer [render-frame-svg]]
|
||||
[app.main.ui.handoff.right-sidebar :refer [right-sidebar]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.viewer.header :refer [header]]
|
||||
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
|
||||
[app.main.ui.viewer.handoff.render :refer [render-frame-svg]]
|
||||
[app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]]
|
||||
[app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]])
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn handle-select-frame [frame]
|
||||
|
@ -37,7 +36,7 @@
|
|||
(st/emit! (dv/select-shape (:id frame)))))
|
||||
|
||||
(mf/defc render-panel
|
||||
[{:keys [data local index page-id file-id]}]
|
||||
[{:keys [data state index page-id file-id]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
frames (:frames data [])
|
||||
objects (:objects data)
|
||||
|
@ -65,26 +64,23 @@
|
|||
[:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)}
|
||||
[:div.handoff-svg-container
|
||||
[:& render-frame-svg {:frame-id (:id frame)
|
||||
:zoom (:zoom local)
|
||||
:zoom (:zoom state)
|
||||
:objects objects}]]]
|
||||
[:& right-sidebar {:frame frame
|
||||
:page-id page-id
|
||||
:file-id file-id}]])]))
|
||||
|
||||
(mf/defc handoff-content
|
||||
[{:keys [data local index page-id file-id] :as props}]
|
||||
|
||||
(let [container (mf/use-ref)
|
||||
[toggle-fullscreen fullscreen?] (hooks/use-fullscreen container)
|
||||
|
||||
on-mouse-wheel
|
||||
[{:keys [data state index page-id file-id] :as props}]
|
||||
(let [on-mouse-wheel
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(when (kbd/ctrl? event)
|
||||
(dom/prevent-default event)
|
||||
(let [event (.getBrowserEvent ^js event)]
|
||||
(if (pos? (.-deltaY ^js event))
|
||||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
(st/emit! dv/increase-zoom))))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
|
@ -98,37 +94,39 @@
|
|||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts dv/shortcuts)
|
||||
|
||||
[:div.handoff-layout {:class (classnames :fullscreen fullscreen?)
|
||||
:ref container}
|
||||
[:& header {:data data
|
||||
:toggle-fullscreen toggle-fullscreen
|
||||
:fullscreen? fullscreen?
|
||||
:local local
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
[:div.handoff-layout
|
||||
[:& header
|
||||
{:data data
|
||||
:state state
|
||||
:index index
|
||||
:screen :handoff}]
|
||||
:section :handoff}]
|
||||
[:div.viewer-content
|
||||
(when (:show-thumbnails local)
|
||||
(when (:show-thumbnails state)
|
||||
[:& thumbnails-panel {:index index
|
||||
:data data
|
||||
:screen :handoff}])
|
||||
[:& render-panel {:data data
|
||||
:local local
|
||||
:state state
|
||||
:index index
|
||||
:page-id page-id
|
||||
:file-id file-id}]]]))
|
||||
:file-id file-id}]]]]))
|
||||
|
||||
(mf/defc handoff
|
||||
[{:keys [file-id page-id index] :as props}]
|
||||
[{:keys [file-id page-id index token] :as props}]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id page-id)
|
||||
(mf/deps file-id page-id token)
|
||||
(fn []
|
||||
(st/emit! (dv/initialize props))))
|
||||
|
||||
(let [data (mf/deref refs/viewer-data)
|
||||
local (mf/deref refs/viewer-local)]
|
||||
(when data
|
||||
[:& handoff-content {:file-id file-id
|
||||
state (mf/deref refs/viewer-local)]
|
||||
|
||||
(when (and data state)
|
||||
[:& handoff-content
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:index index
|
||||
:local local
|
||||
:state state
|
||||
:data data}])))
|
|
@ -7,19 +7,19 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes
|
||||
(ns app.main.ui.handoff.attributes
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[app.util.i18n :as i18n]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.viewer.handoff.exports :refer [exports]]
|
||||
[app.main.ui.viewer.handoff.attributes.layout :refer [layout-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.fill :refer [fill-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.stroke :refer [stroke-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.viewer.handoff.attributes.text :refer [text-panel]]))
|
||||
[app.main.ui.handoff.exports :refer [exports]]
|
||||
[app.main.ui.handoff.attributes.layout :refer [layout-panel]]
|
||||
[app.main.ui.handoff.attributes.fill :refer [fill-panel]]
|
||||
[app.main.ui.handoff.attributes.stroke :refer [stroke-panel]]
|
||||
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.handoff.attributes.text :refer [text-panel]]))
|
||||
|
||||
(def type->options
|
||||
{:multiple [:fill :stroke :image :text :shadow :blur]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.blur
|
||||
(ns app.main.ui.handoff.attributes.blur
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.common
|
||||
(ns app.main.ui.handoff.attributes.common
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.fill
|
||||
(ns app.main.ui.handoff.attributes.fill
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[app.util.i18n :refer [t]]
|
||||
|
@ -15,7 +15,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]))
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]))
|
||||
|
||||
(def fill-attributes [:fill-color :fill-color-gradient])
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.image
|
||||
(ns app.main.ui.handoff.attributes.image
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.layout
|
||||
(ns app.main.ui.handoff.attributes.layout
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.shadow
|
||||
(ns app.main.ui.handoff.attributes.shadow
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -16,7 +16,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]))
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]))
|
||||
|
||||
(defn has-shadow? [shape]
|
||||
(:shadow shape))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.stroke
|
||||
(ns app.main.ui.handoff.attributes.stroke
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -16,7 +16,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]))
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]))
|
||||
|
||||
(defn shape->color [shape]
|
||||
{:color (:stroke-color shape)
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.attributes.text
|
||||
(ns app.main.ui.handoff.attributes.text
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -19,7 +19,7 @@
|
|||
[app.main.fonts :as fonts]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.webapi :as wapi]
|
||||
[app.main.ui.viewer.handoff.attributes.common :refer [color-row]]
|
||||
[app.main.ui.handoff.attributes.common :refer [color-row]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.copy-button :refer [copy-button]]))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.code
|
||||
(ns app.main.ui.handoff.code
|
||||
(:require
|
||||
["js-beautify" :as beautify]
|
||||
[cuerdas.core :as str]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.exports
|
||||
(ns app.main.ui.handoff.exports
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[beicon.core :as rx]
|
|
@ -7,18 +7,18 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.left-sidebar
|
||||
(ns app.main.ui.handoff.left-sidebar
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[okulary.core :as l]
|
||||
[app.common.data :as d]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]]))
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]]
|
||||
[app.util.dom :as dom]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def selected-shapes
|
||||
(l/derived (comp :selected :viewer-local) st/state))
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.render
|
||||
(ns app.main.ui.handoff.render
|
||||
"The main container for a frame in handoff mode"
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
|
@ -30,7 +30,7 @@
|
|||
[app.main.ui.shapes.path :as path]
|
||||
[app.main.ui.shapes.rect :as rect]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]]
|
||||
[app.main.ui.handoff.selection-feedback :refer [selection-feedback]]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]))
|
||||
|
||||
(declare shape-container-factory)
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.right-sidebar
|
||||
(ns app.main.ui.handoff.right-sidebar
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[okulary.core :as l]
|
||||
|
@ -16,8 +16,8 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
[app.main.ui.workspace.sidebar.layers :refer [element-icon]]
|
||||
[app.main.ui.viewer.handoff.attributes :refer [attributes]]
|
||||
[app.main.ui.viewer.handoff.code :refer [code]]))
|
||||
[app.main.ui.handoff.attributes :refer [attributes]]
|
||||
[app.main.ui.handoff.code :refer [code]]))
|
||||
|
||||
(defn make-selected-shapes-iref
|
||||
[]
|
|
@ -7,7 +7,7 @@
|
|||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.viewer.handoff.selection-feedback
|
||||
(ns app.main.ui.handoff.selection-feedback
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
|
@ -50,23 +50,6 @@
|
|||
(fn [] (mousetrap/reset))))
|
||||
nil)
|
||||
|
||||
(defn use-fullscreen
|
||||
[ref]
|
||||
(let [state (mf/use-state (dom/fullscreen?))
|
||||
change (mf/use-callback #(reset! state (dom/fullscreen?)))
|
||||
toggle (mf/use-callback (mf/deps @state)
|
||||
#(let [el (mf/ref-val ref)]
|
||||
(swap! state not)
|
||||
(if @state
|
||||
(wapi/exit-fullscreen)
|
||||
(wapi/request-fullscreen el))))]
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(.addEventListener js/document "fullscreenchange" change)
|
||||
#(.removeEventListener js/document "fullscreenchange" change)))
|
||||
|
||||
[toggle @state]))
|
||||
|
||||
(defn invisible-image
|
||||
[]
|
||||
(let [img (js/Image.)
|
||||
|
|
|
@ -9,31 +9,182 @@
|
|||
|
||||
(ns app.main.ui.viewer
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.pages-helpers :as cph]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.fullscreen :as fs]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.viewer.header :refer [header]]
|
||||
[app.main.ui.viewer.shapes :refer [frame-svg]]
|
||||
[app.main.ui.viewer.shapes :as shapes :refer [frame-svg]]
|
||||
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf])
|
||||
(:import goog.events.EventType))
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- frame-contains?
|
||||
[{:keys [x y width height]} {px :x py :y}]
|
||||
(let [x2 (+ x width)
|
||||
y2 (+ y height)]
|
||||
(and (<= x px x2)
|
||||
(<= y py y2))))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def comments-local-ref
|
||||
(l/derived :comments-local st/state))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [width height zoom frame data] :as props}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
|
||||
modifier1 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
modifier2 (-> (gpt/point (:x frame) (:y frame))
|
||||
(gmt/translate-matrix))
|
||||
|
||||
threads-map (->> (mf/deref threads-ref)
|
||||
(d/mapm #(update %2 :position gpt/transform modifier1)))
|
||||
|
||||
cstate (mf/deref refs/comments-local)
|
||||
|
||||
mframe (geom/transform-shape frame)
|
||||
threads (->> (vals threads-map)
|
||||
(dcm/apply-filters cstate profile)
|
||||
(filter (fn [{:keys [seqn position]}]
|
||||
(frame-contains? mframe position))))
|
||||
|
||||
on-bubble-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(fn [thread]
|
||||
(if (= (:open cstate) (:id thread))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread)))))
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
(mf/deps cstate data)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (some? (:open cstate))
|
||||
(st/emit! (dcm/close-thread))
|
||||
(let [event (.-nativeEvent ^js event)
|
||||
position (-> (dom/get-offset-position event)
|
||||
(gpt/transform modifier2))
|
||||
params {:position position
|
||||
:page-id (get-in data [:page :id])
|
||||
:file-id (get-in data [:file :id])}]
|
||||
(st/emit! (dcm/create-draft params))))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(mf/deps cstate)
|
||||
(st/emitf (dcm/close-thread)))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(fn [draft]
|
||||
(let [params (update draft :position gpt/transform modifier2)]
|
||||
;; (prn "on-draft-submit" params)
|
||||
|
||||
(st/emit! (dcm/create-thread params)
|
||||
(dcm/close-thread)))))
|
||||
]
|
||||
|
||||
[:div.viewer-comments {:on-click on-click}
|
||||
[:div.comments-layer
|
||||
[:div.threads
|
||||
(for [item threads]
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open cstate))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open cstate)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:users (:users data)
|
||||
:zoom zoom}]))
|
||||
|
||||
(when-let [draft (:draft cstate)]
|
||||
[:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1)
|
||||
:on-cancel on-draft-cancel
|
||||
:on-submit on-draft-submit
|
||||
:zoom zoom}])]]]))
|
||||
|
||||
|
||||
|
||||
(mf/defc viewport
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [state data index section] :or {zoom 1} :as props}]
|
||||
(let [zoom (:zoom state)
|
||||
objects (:objects data)
|
||||
|
||||
frame (get-in data [:frames index])
|
||||
frame-id (:id frame)
|
||||
|
||||
modifier (-> (gpt/point (:x frame) (:y frame))
|
||||
(gpt/negate)
|
||||
(gmt/translate-matrix))
|
||||
|
||||
update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier)
|
||||
|
||||
objects (->> (d/concat [frame-id] (cph/get-children frame-id objects))
|
||||
(reduce update-fn objects))
|
||||
|
||||
interactions? (:interactions-show? state)
|
||||
wrapper (mf/use-memo (mf/deps objects) #(shapes/frame-container-factory objects interactions?))
|
||||
|
||||
;; Retrieve frame again with correct modifier
|
||||
frame (get objects frame-id)
|
||||
|
||||
width (* (:width frame) zoom)
|
||||
height (* (:height frame) zoom)
|
||||
vbox (str "0 0 " (:width frame 0) " " (:height frame 0))]
|
||||
|
||||
[:div.viewport-container
|
||||
{:style {:width width
|
||||
:height height
|
||||
:state state
|
||||
:position "relative"}}
|
||||
|
||||
(when (= section :comments)
|
||||
[:& comments-layer {:width width
|
||||
:height height
|
||||
:frame frame
|
||||
:data data
|
||||
:zoom zoom}])
|
||||
|
||||
[:svg {:view-box vbox
|
||||
:width width
|
||||
:height height
|
||||
:version "1.1"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns "http://www.w3.org/2000/svg"}
|
||||
[:& wrapper {:shape frame
|
||||
:show-interactions? interactions?
|
||||
:view-box vbox}]]]))
|
||||
|
||||
(mf/defc main-panel
|
||||
[{:keys [data local index]}]
|
||||
[{:keys [data state index section]}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
frames (:frames data [])
|
||||
objects (:objects data)
|
||||
frames (:frames data)
|
||||
frame (get frames index)]
|
||||
[:section.viewer-preview
|
||||
(cond
|
||||
|
@ -45,22 +196,20 @@
|
|||
[:section.empty-state
|
||||
[:span (t locale "viewer.frame-not-found")]]
|
||||
|
||||
:else
|
||||
[:& frame-svg {:frame frame
|
||||
:show-interactions? (:show-interactions? local)
|
||||
:zoom (:zoom local)
|
||||
:objects objects}])]))
|
||||
(some? state)
|
||||
[:& viewport
|
||||
{:data data
|
||||
:section section
|
||||
:index index
|
||||
:state state
|
||||
}])]))
|
||||
|
||||
(mf/defc viewer-content
|
||||
[{:keys [data local index] :as props}]
|
||||
(let [container (mf/use-ref)
|
||||
|
||||
[toggle-fullscreen fullscreen?] (hooks/use-fullscreen container)
|
||||
|
||||
on-click
|
||||
[{:keys [data state index section] :as props}]
|
||||
(let [on-click
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [mode (get local :interactions-mode)]
|
||||
(let [mode (get state :interactions-mode)]
|
||||
(when (= mode :show-on-click)
|
||||
(st/emit! dv/flash-interactions))))
|
||||
|
||||
|
@ -73,49 +222,63 @@
|
|||
(st/emit! dv/decrease-zoom)
|
||||
(st/emit! dv/increase-zoom)))))
|
||||
|
||||
on-click
|
||||
(fn [event]
|
||||
(st/emit! (dcm/close-thread)))
|
||||
|
||||
on-key-down
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(st/emit! (dcm/close-thread))))
|
||||
|
||||
on-mount
|
||||
(fn []
|
||||
;; bind with passive=false to allow the event to be cancelled
|
||||
;; https://stackoverflow.com/a/57582286/3219895
|
||||
(let [key1 (events/listen goog/global EventType.WHEEL
|
||||
on-mouse-wheel #js {"passive" false})]
|
||||
(let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false})
|
||||
key2 (events/listen js/document "keydown" on-key-down)
|
||||
key3 (events/listen js/document "click" on-click)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1))))]
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2)
|
||||
(events/unlistenByKey key3))))]
|
||||
|
||||
(mf/use-effect on-mount)
|
||||
(hooks/use-shortcuts dv/shortcuts)
|
||||
|
||||
[:div.viewer-layout {:class (classnames :fullscreen fullscreen?)
|
||||
:ref container}
|
||||
[:& fs/fullscreen-wrapper {}
|
||||
[:div.viewer-layout
|
||||
[:& header
|
||||
{:data data
|
||||
:state state
|
||||
:section section
|
||||
:index index}]
|
||||
|
||||
[:& header {:data data
|
||||
:toggle-fullscreen toggle-fullscreen
|
||||
:fullscreen? fullscreen?
|
||||
:local local
|
||||
:index index
|
||||
:screen :viewer}]
|
||||
[:div.viewer-content {:on-click on-click}
|
||||
(when (:show-thumbnails local)
|
||||
(when (:show-thumbnails state)
|
||||
[:& thumbnails-panel {:screen :viewer
|
||||
:index index
|
||||
:data data}])
|
||||
[:& main-panel {:data data
|
||||
:local local
|
||||
:index index}]]]))
|
||||
:section section
|
||||
:state state
|
||||
:index index}]]]]))
|
||||
|
||||
|
||||
;; --- Component: Viewer Page
|
||||
|
||||
(mf/defc viewer-page
|
||||
[{:keys [file-id page-id index token] :as props}]
|
||||
[{:keys [file-id page-id index token section] :as props}]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id page-id token)
|
||||
(fn []
|
||||
(st/emit! (dv/initialize props))))
|
||||
(st/emitf (dv/initialize props)))
|
||||
|
||||
(let [data (mf/deref refs/viewer-data)
|
||||
local (mf/deref refs/viewer-local)]
|
||||
(when data
|
||||
[:& viewer-content {:index index
|
||||
:local local
|
||||
state (mf/deref refs/viewer-local)]
|
||||
(when (and data state)
|
||||
[:& viewer-content
|
||||
{:index index
|
||||
:section section
|
||||
:state state
|
||||
:data data}])))
|
||||
|
|
|
@ -9,21 +9,23 @@
|
|||
|
||||
(ns app.main.ui.viewer.header
|
||||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.fullscreen :as fs]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [t]]
|
||||
[app.util.router :as rt]
|
||||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.webapi :as wapi]))
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc zoom-widget
|
||||
{:wrap [mf/memo]}
|
||||
|
@ -40,7 +42,7 @@
|
|||
[:span.dropdown-button i/arrow-down]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(reset! show-dropdown? false)}
|
||||
[:ul.zoom-dropdown
|
||||
[:ul.dropdown.zoom-dropdown
|
||||
[:li {:on-click on-increase}
|
||||
"Zoom in" [:span "+"]]
|
||||
[:li {:on-click on-decrease}
|
||||
|
@ -52,29 +54,6 @@
|
|||
[:li {:on-click on-zoom-to-200}
|
||||
"Zoom to 200%" [:span "Shift + 2"]]]]]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [interactions-mode] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
locale (i18n/use-locale)
|
||||
on-select-mode #(st/emit! (dv/set-interactions-mode %))]
|
||||
[:div.header-icon
|
||||
[:a {:on-click #(swap! show-dropdown? not)} i/eye
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)}
|
||||
[:ul.custom-select-dropdown
|
||||
[:li {:key :hide
|
||||
:class (classnames :selected (= interactions-mode :hide))
|
||||
:on-click #(on-select-mode :hide)}
|
||||
(t locale "viewer.header.dont-show-interactions")]
|
||||
[:li {:key :show
|
||||
:class (classnames :selected (= interactions-mode :show))
|
||||
:on-click #(on-select-mode :show)}
|
||||
(t locale "viewer.header.show-interactions")]
|
||||
[:li {:key :show-on-click
|
||||
:class (classnames :selected (= interactions-mode :show-on-click))
|
||||
:on-click #(on-select-mode :show-on-click)}
|
||||
(t locale "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
||||
(mf/defc share-link
|
||||
[{:keys [page token] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
|
@ -103,7 +82,7 @@
|
|||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close #(swap! show-dropdown? not)
|
||||
:container dropdown-ref}
|
||||
[:div.share-link-dropdown {:ref dropdown-ref}
|
||||
[:div.dropdown.share-link-dropdown {:ref dropdown-ref}
|
||||
[:span.share-link-title (t locale "viewer.header.share.title")]
|
||||
[:div.share-link-input
|
||||
(if (string? token)
|
||||
|
@ -121,16 +100,87 @@
|
|||
[:button.btn-primary {:on-click create}
|
||||
(t locale "viewer.header.share.create-link")])]]]]))
|
||||
|
||||
(mf/defc interactions-menu
|
||||
[{:keys [state locale] :as props}]
|
||||
(let [imode (:interactions-mode state)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
show-dropdown (mf/use-fn #(reset! show-dropdown? true))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
select-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dv/set-interactions-mode mode))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.icon {:on-click #(swap! show-dropdown? not)} i/eye]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= imode :hide))
|
||||
:on-click #(select-mode :hide)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "viewer.header.dont-show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show))
|
||||
:on-click #(select-mode :show)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "viewer.header.show-interactions")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= imode :show-on-click))
|
||||
:on-click #(select-mode :show-on-click)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "viewer.header.show-interactions-on-click")]]]]]))
|
||||
|
||||
|
||||
(mf/defc comments-menu
|
||||
[{:keys [locale] :as props}]
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
|
||||
show-dropdown? (mf/use-state false)
|
||||
show-dropdown (mf/use-fn #(reset! show-dropdown? true))
|
||||
hide-dropdown (mf/use-fn #(reset! show-dropdown? false))
|
||||
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:div.view-options
|
||||
[:div.icon {:on-click #(swap! show-dropdown? not)} i/eye]
|
||||
[:& dropdown {:show @show-dropdown?
|
||||
:on-close hide-dropdown}
|
||||
[:ul.dropdown.with-check
|
||||
[:li {:class (dom/classnames :selected (= :all cmode))
|
||||
:on-click #(update-mode :all)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "labels.show-all-comments")]]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :yours cmode))
|
||||
:on-click #(update-mode :yours)}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "labels.show-your-comments")]]
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "labels.hide-resolved-comments")]]]]]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [data index local fullscreen? toggle-fullscreen screen] :as props}]
|
||||
[{:keys [data index section state] :as props}]
|
||||
(let [{:keys [project file page frames]} data
|
||||
|
||||
fullscreen (mf/use-ctx fs/fullscreen-context)
|
||||
total (count frames)
|
||||
on-click #(st/emit! dv/toggle-thumbnails-panel)
|
||||
|
||||
interactions-mode (:interactions-mode local)
|
||||
|
||||
locale (i18n/use-locale)
|
||||
|
||||
locale (mf/deref i18n/locale)
|
||||
profile (mf/deref refs/profile)
|
||||
anonymous? (= uuid/zero (:id profile))
|
||||
|
||||
|
@ -138,17 +188,34 @@
|
|||
file-id (get-in data [:file :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
|
||||
:file-id file-id}
|
||||
{:page-id page-id}))
|
||||
|
||||
change-screen
|
||||
(fn [screen]
|
||||
{:page-id page-id})))
|
||||
navigate
|
||||
(mf/use-callback
|
||||
(fn [section]
|
||||
(st/emit!
|
||||
(rt/nav screen
|
||||
(case section
|
||||
:interactions
|
||||
(rt/nav :viewer
|
||||
{: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
|
||||
[:div.main-icon
|
||||
[:a {:on-click on-edit} i/logo-icon]]
|
||||
|
@ -160,19 +227,33 @@
|
|||
[:span.file-name (:name file)]
|
||||
[:span "/"]
|
||||
[:span.page-name (:name page)]
|
||||
[:span.dropdown-button i/arrow-down]
|
||||
[:span.show-thumbnails-button i/arrow-down]
|
||||
[:span.counters (str (inc index) " / " total)]]
|
||||
|
||||
[:div.mode-zone
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :viewer)
|
||||
(change-screen :viewer))
|
||||
:class (when (= screen :viewer) "active") :alt "View mode"} i/play]
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom {:on-click #(when (not= screen :handoff)
|
||||
(change-screen :handoff))
|
||||
:class (when (= screen :handoff) "active") :alt "Code mode"} i/code]]
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :interactions)
|
||||
:class (dom/classnames :active (= section :interactions))
|
||||
:alt "View mode"}
|
||||
i/play]
|
||||
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :comments)
|
||||
:class (dom/classnames :active (= section :comments))
|
||||
:alt "Comments"}
|
||||
i/chat]
|
||||
|
||||
[:button.mode-zone-button.tooltip.tooltip-bottom
|
||||
{:on-click #(navigate :handoff)
|
||||
:class (dom/classnames :active (= section :handoff))
|
||||
:alt "Code mode"}
|
||||
i/code]]
|
||||
|
||||
[:div.options-zone
|
||||
[:& interactions-menu {:interactions-mode interactions-mode}]
|
||||
(case section
|
||||
:interactions [:& interactions-menu {:state state :locale locale}]
|
||||
:comments [:& comments-menu {:locale locale}]
|
||||
nil)
|
||||
|
||||
(when-not anonymous?
|
||||
[:& share-link {:token (:share-token data)
|
||||
|
@ -183,17 +264,17 @@
|
|||
(t locale "viewer.header.edit-page")])
|
||||
|
||||
[:& zoom-widget
|
||||
{:zoom (:zoom local)
|
||||
:on-increase #(st/emit! dv/increase-zoom)
|
||||
:on-decrease #(st/emit! dv/decrease-zoom)
|
||||
:on-zoom-to-50 #(st/emit! dv/zoom-to-50)
|
||||
:on-zoom-to-100 #(st/emit! dv/reset-zoom)
|
||||
:on-zoom-to-200 #(st/emit! dv/zoom-to-200)}]
|
||||
{:zoom (:zoom state)
|
||||
:on-increase (st/emitf dv/increase-zoom)
|
||||
:on-decrease (st/emitf dv/decrease-zoom)
|
||||
:on-zoom-to-50 (st/emitf dv/zoom-to-50)
|
||||
:on-zoom-to-100 (st/emitf dv/reset-zoom)
|
||||
:on-zoom-to-200 (st/emitf dv/zoom-to-200)}]
|
||||
|
||||
[:span.btn-icon-dark.btn-small.tooltip.tooltip-bottom
|
||||
{:alt (t locale "viewer.header.fullscreen")
|
||||
:on-click toggle-fullscreen}
|
||||
(if fullscreen?
|
||||
:on-click #(if @fullscreen (fullscreen false) (fullscreen true))}
|
||||
(if @fullscreen
|
||||
i/full-screen-off
|
||||
i/full-screen)]
|
||||
]]))
|
||||
|
|
|
@ -111,8 +111,7 @@
|
|||
on-item-click
|
||||
(fn [event index]
|
||||
(compare-and-set! selected false true)
|
||||
(st/emit! (rt/nav screen {:file-id file-id
|
||||
:page-id page-id} {:index index}))
|
||||
(st/emit! (dv/go-to-frame-by-index index))
|
||||
(when @expanded?
|
||||
(on-close)))]
|
||||
[:& dropdown' {:on-close on-close
|
||||
|
|
|
@ -12,362 +12,54 @@
|
|||
[app.config :as cfg]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.keyboard :as kbd]
|
||||
[app.main.ui.workspace.colorpicker]
|
||||
[app.main.ui.workspace.context-menu :refer [context-menu]]
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[beicon.core :as rx]
|
||||
[app.util.i18n :as i18n :refer [t tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(declare group-threads-by-page)
|
||||
(declare apply-filters)
|
||||
|
||||
(mf/defc resizing-textarea
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [value (obj/get props "value" "")
|
||||
on-focus (obj/get props "on-focus")
|
||||
on-blur (obj/get props "on-blur")
|
||||
placeholder (obj/get props "placeholder")
|
||||
on-change (obj/get props "on-change")
|
||||
|
||||
on-esc (obj/get props "on-esc")
|
||||
|
||||
ref (mf/use-ref)
|
||||
;; state (mf/use-state value)
|
||||
|
||||
on-key-down
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(when (and (kbd/esc? event)
|
||||
(fn? on-esc))
|
||||
(on-esc event))))
|
||||
|
||||
on-change*
|
||||
(mf/use-callback
|
||||
(mf/deps on-change)
|
||||
(fn [event]
|
||||
(let [content (dom/get-target-val event)]
|
||||
(on-change content))))]
|
||||
|
||||
|
||||
(mf/use-layout-effect
|
||||
nil
|
||||
(fn []
|
||||
(let [node (mf/ref-val ref)]
|
||||
(set! (.-height (.-style node)) "0")
|
||||
(set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px")))))
|
||||
|
||||
[:textarea
|
||||
{:ref ref
|
||||
:on-key-down on-key-down
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:value value
|
||||
:placeholder placeholder
|
||||
:on-change on-change*}]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Workspace
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc reply-form
|
||||
[{:keys [thread] :as props}]
|
||||
(let [show-buttons? (mf/use-state false)
|
||||
content (mf/use-state "")
|
||||
|
||||
on-focus
|
||||
(mf/use-callback
|
||||
#(reset! show-buttons? true))
|
||||
|
||||
on-blur
|
||||
(mf/use-callback
|
||||
#(reset! show-buttons? false))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
#(reset! content %))
|
||||
|
||||
on-cancel
|
||||
(mf/use-callback
|
||||
#(do (reset! content "")
|
||||
(reset! show-buttons? false)))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/deps thread @content)
|
||||
(fn []
|
||||
(st/emit! (dwcm/add-comment thread @content))
|
||||
(on-cancel)))]
|
||||
|
||||
[:div.reply-form
|
||||
[:& resizing-textarea {:value @content
|
||||
:placeholder "Reply"
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus
|
||||
:on-change on-change}]
|
||||
(when (or @show-buttons?
|
||||
(not (empty? @content)))
|
||||
[:div.buttons
|
||||
[:input.btn-primary {:type "button" :value "Post" :on-click on-submit}]
|
||||
[:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])]))
|
||||
|
||||
(mf/defc draft-thread
|
||||
[{:keys [draft zoom] :as props}]
|
||||
(let [position (:position draft)
|
||||
content (:content draft)
|
||||
pos-x (* (:x position) zoom)
|
||||
pos-y (* (:y position) zoom)
|
||||
|
||||
on-esc
|
||||
(mf/use-callback
|
||||
(mf/deps draft)
|
||||
(st/emitf :interrupt))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(mf/deps draft)
|
||||
(fn [content]
|
||||
(st/emit! (dwcm/update-draft-thread (assoc draft :content content)))))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/deps draft)
|
||||
(st/emitf (dwcm/create-thread draft)))]
|
||||
|
||||
[:*
|
||||
[:div.thread-bubble
|
||||
{:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}}
|
||||
[:span "?"]]
|
||||
[:div.thread-content
|
||||
{:style {:top (str (- pos-y 14) "px")
|
||||
:left (str (+ pos-x 14) "px")}}
|
||||
[:div.reply-form
|
||||
[:& resizing-textarea {:placeholder "Write new comment"
|
||||
:value content
|
||||
:on-esc on-esc
|
||||
:on-change on-change}]
|
||||
[:div.buttons
|
||||
[:input.btn-primary
|
||||
{:on-click on-submit
|
||||
:type "button"
|
||||
:value "Post"}]
|
||||
[:input.btn-secondary
|
||||
{:on-click on-esc
|
||||
:type "button"
|
||||
:value "Cancel"}]]]]]))
|
||||
|
||||
|
||||
(mf/defc edit-form
|
||||
[{:keys [content on-submit on-cancel] :as props}]
|
||||
(let [content (mf/use-state content)
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
#(reset! content %))
|
||||
|
||||
on-submit*
|
||||
(mf/use-callback
|
||||
(mf/deps @content)
|
||||
(fn [] (on-submit @content)))]
|
||||
|
||||
[:div.reply-form.edit-form
|
||||
[:& resizing-textarea {:value @content
|
||||
:on-change on-change}]
|
||||
[:div.buttons
|
||||
[:input.btn-primary {:type "button" :value "Post" :on-click on-submit*}]
|
||||
[:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]]))
|
||||
|
||||
|
||||
(mf/defc comment-item
|
||||
[{:keys [comment thread] :as props}]
|
||||
(let [profile (get @refs/workspace-users (:owner-id comment))
|
||||
|
||||
options (mf/use-state false)
|
||||
edition? (mf/use-state false)
|
||||
|
||||
on-show-options
|
||||
(mf/use-callback #(reset! options true))
|
||||
|
||||
on-hide-options
|
||||
(mf/use-callback #(reset! options false))
|
||||
|
||||
on-edit-clicked
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(reset! options false)
|
||||
(reset! edition? true)))
|
||||
|
||||
on-delete-comment
|
||||
(mf/use-callback
|
||||
(mf/deps comment)
|
||||
(st/emitf (dwcm/delete-comment comment)))
|
||||
|
||||
delete-thread
|
||||
(mf/use-callback
|
||||
(mf/deps thread)
|
||||
(st/emitf (dwcm/close-thread)
|
||||
(dwcm/delete-comment-thread thread)))
|
||||
|
||||
|
||||
on-delete-thread
|
||||
(mf/use-callback
|
||||
(mf/deps thread)
|
||||
(st/emitf (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-comment-thread.title")
|
||||
:message (tr "modals.delete-comment-thread.message")
|
||||
:accept-label (tr "modals.delete-comment-thread.accept")
|
||||
:on-accept delete-thread})))
|
||||
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(mf/deps comment thread)
|
||||
(fn [content]
|
||||
(reset! edition? false)
|
||||
(st/emit! (dwcm/update-comment (assoc comment :content content)))))
|
||||
|
||||
on-cancel
|
||||
(mf/use-callback #(reset! edition? false))
|
||||
|
||||
toggle-resolved
|
||||
(mf/use-callback
|
||||
(mf/deps thread)
|
||||
(st/emitf (dwcm/update-comment-thread (update thread :is-resolved not))))]
|
||||
|
||||
[:div.comment-container
|
||||
[:div.comment
|
||||
[:div.author
|
||||
[:div.avatar
|
||||
[:img {:src (cfg/resolve-media-path (:photo profile))}]]
|
||||
[:div.name
|
||||
[:div.fullname (:fullname profile)]
|
||||
[:div.timeago (dt/timeago (:modified-at comment))]]
|
||||
|
||||
(when (some? thread)
|
||||
[:div.options-resolve {:on-click toggle-resolved}
|
||||
(if (:is-resolved thread)
|
||||
[:span i/checkbox-checked]
|
||||
[:span i/checkbox-unchecked])])
|
||||
|
||||
[:div.options
|
||||
[:div.options-icon {:on-click on-show-options} i/actions]]]
|
||||
|
||||
[:div.content
|
||||
(if @edition?
|
||||
[:& edit-form {:content (:content comment)
|
||||
:on-submit on-submit
|
||||
:on-cancel on-cancel}]
|
||||
[:span.text (:content comment)])]]
|
||||
|
||||
[:& dropdown {:show @options
|
||||
:on-close on-hide-options}
|
||||
[:ul.dropdown.comment-options-dropdown
|
||||
[:li {:on-click on-edit-clicked} "Edit"]
|
||||
(if thread
|
||||
[:li {:on-click on-delete-thread} "Delete thread"]
|
||||
[:li {:on-click on-delete-comment} "Delete comment"])]]]))
|
||||
|
||||
(defn comments-ref
|
||||
[{:keys [id] :as thread}]
|
||||
(l/derived (l/in [:comments id]) st/state))
|
||||
|
||||
(mf/defc thread-comments
|
||||
[{:keys [thread zoom]}]
|
||||
(let [ref (mf/use-ref)
|
||||
pos (:position thread)
|
||||
pos-x (+ (* (:x pos) zoom) 14)
|
||||
pos-y (- (* (:y pos) zoom) 14)
|
||||
|
||||
comments-ref (mf/use-memo (mf/deps thread) #(comments-ref thread))
|
||||
comments-map (mf/deref comments-ref)
|
||||
comments (->> (vals comments-map)
|
||||
(sort-by :created-at))
|
||||
comment (first comments)]
|
||||
|
||||
(mf/use-effect
|
||||
(st/emitf (dwcm/update-comment-thread-status thread)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thread)
|
||||
(st/emitf (dwcm/retrieve-comments (:id thread))))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps thread comments-map)
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(.scrollIntoView ^js node))))
|
||||
|
||||
[:div.thread-content
|
||||
{:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}}
|
||||
|
||||
[:div.comments
|
||||
[:& comment-item {:comment comment
|
||||
:thread thread}]
|
||||
(for [item (rest comments)]
|
||||
[:*
|
||||
[:hr]
|
||||
[:& comment-item {:comment item}]])
|
||||
[:div {:ref ref}]]
|
||||
[:& reply-form {:thread thread}]]))
|
||||
|
||||
(mf/defc thread-bubble
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [thread zoom open?] :as params}]
|
||||
(let [pos (:position thread)
|
||||
pos-x (* (:x pos) zoom)
|
||||
pos-y (* (:y pos) zoom)
|
||||
|
||||
on-open-toggle
|
||||
(mf/use-callback
|
||||
(mf/deps thread open?)
|
||||
(fn []
|
||||
(if open?
|
||||
(st/emit! (dwcm/close-thread))
|
||||
(st/emit! (dwcm/open-thread thread)))))]
|
||||
|
||||
[:div.thread-bubble
|
||||
{:style {:top (str pos-y "px")
|
||||
:left (str pos-x "px")}
|
||||
:class (dom/classnames
|
||||
:resolved (:is-resolved thread)
|
||||
:unread (pos? (:count-unread-comments thread)))
|
||||
:on-click on-open-toggle}
|
||||
[:span (:seqn thread)]]))
|
||||
|
||||
(def threads-ref
|
||||
(l/derived :comment-threads st/state))
|
||||
|
||||
(def workspace-comments-ref
|
||||
(l/derived :workspace-comments st/state))
|
||||
|
||||
(mf/defc comments-layer
|
||||
[{:keys [vbox vport zoom file-id page-id drawing] :as props}]
|
||||
(let [pos-x (* (- (:x vbox)) zoom)
|
||||
pos-y (* (- (:y vbox)) zoom)
|
||||
|
||||
profile (mf/deref refs/profile)
|
||||
local (mf/deref workspace-comments-ref)
|
||||
local (mf/deref refs/comments-local)
|
||||
threads-map (mf/deref threads-ref)
|
||||
|
||||
threads (->> (vals threads-map)
|
||||
(filter #(= (:page-id %) page-id))
|
||||
(apply-filters local profile))]
|
||||
(dcm/apply-filters local profile))
|
||||
|
||||
on-bubble-click
|
||||
(fn [{:keys [id] :as thread}]
|
||||
(if (= (:open local) id)
|
||||
(st/emit! (dcm/close-thread))
|
||||
(st/emit! (dcm/open-thread thread))))
|
||||
|
||||
on-draft-cancel
|
||||
(mf/use-callback
|
||||
(st/emitf :interrupt))
|
||||
|
||||
on-draft-submit
|
||||
(mf/use-callback
|
||||
(fn [draft]
|
||||
(st/emit! (dcm/create-thread draft)
|
||||
#_(dcm/close-thread))))
|
||||
]
|
||||
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
|
@ -377,22 +69,27 @@
|
|||
(st/emit! ::dwcm/finalize))))
|
||||
|
||||
[:div.workspace-comments
|
||||
[:div.comments-layer
|
||||
{:style {:width (str (:width vport) "px")
|
||||
:height (str (:height vport) "px")}}
|
||||
[:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}}
|
||||
(for [item threads]
|
||||
[:& thread-bubble {:thread item
|
||||
[:& cmt/thread-bubble {:thread item
|
||||
:zoom zoom
|
||||
:on-click on-bubble-click
|
||||
:open? (= (:id item) (:open local))
|
||||
:key (:seqn item)}])
|
||||
|
||||
(when-let [id (:open local)]
|
||||
(when-let [thread (get threads-map id)]
|
||||
[:& thread-comments {:thread thread
|
||||
[:& cmt/thread-comments {:thread thread
|
||||
:zoom zoom}]))
|
||||
|
||||
(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))))
|
||||
(tm/schedule
|
||||
(st/emitf (dwcm/center-to-comment-thread item)
|
||||
(dwcm/open-thread item)))))]
|
||||
(dcm/open-thread item)))))]
|
||||
|
||||
[:div.comment {:on-click on-click}
|
||||
[:div.author
|
||||
|
@ -461,41 +158,49 @@
|
|||
|
||||
(mf/defc sidebar-options
|
||||
[{:keys [local] :as props}]
|
||||
(let [filter-yours
|
||||
(mf/use-callback
|
||||
(mf/deps local)
|
||||
(st/emitf (dwcm/update-filters {:main :yours})))
|
||||
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
|
||||
locale (mf/deref i18n/locale)
|
||||
|
||||
filter-all
|
||||
update-mode
|
||||
(mf/use-callback
|
||||
(mf/deps local)
|
||||
(st/emitf (dwcm/update-filters {:main :all})))
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:mode mode}))))
|
||||
|
||||
toggle-resolved
|
||||
update-show
|
||||
(mf/use-callback
|
||||
(mf/deps local)
|
||||
(st/emitf (dwcm/update-filters {:resolved (not (:filter-resolved local))})))]
|
||||
(fn [mode]
|
||||
(st/emit! (dcm/update-filters {:show mode}))))]
|
||||
|
||||
[:ul.dropdown.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]
|
||||
(if (:filter-resolved local)
|
||||
[:li {:on-click toggle-resolved} "Show resolved comments"]
|
||||
[:li {:on-click toggle-resolved} "Hide resolved comments"])]))
|
||||
|
||||
[:li {:class (dom/classnames :selected (= :pending cshow))
|
||||
:on-click #(update-show (if (= :pending cshow) :all :pending))}
|
||||
[:span.icon i/tick]
|
||||
[:span.label (t locale "labels.hide-resolved-comments")]]]))
|
||||
|
||||
(mf/defc comments-sidebar
|
||||
[]
|
||||
(let [threads-map (mf/deref threads-ref)
|
||||
profile (mf/deref refs/profile)
|
||||
local (mf/deref workspace-comments-ref)
|
||||
local (mf/deref refs/comments-local)
|
||||
options? (mf/use-state false)
|
||||
|
||||
tgroups (->> (vals threads-map)
|
||||
(sort-by :modified-at)
|
||||
(reverse)
|
||||
(apply-filters local profile)
|
||||
(group-threads-by-page))]
|
||||
(dcm/apply-filters local profile)
|
||||
(dcm/group-threads-by-page))]
|
||||
|
||||
[:div.workspace-comments.workspace-comments-sidebar
|
||||
[:div.sidebar-title
|
||||
|
@ -520,30 +225,3 @@
|
|||
:key (:page-id tgroup)}]])])]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Helpers
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- group-threads-by-page
|
||||
[threads]
|
||||
(letfn [(group-by-page [result thread]
|
||||
(let [current (first result)]
|
||||
(if (= (:page-id current) (:page-id thread))
|
||||
(cons (update current :items conj thread)
|
||||
(rest result))
|
||||
(cons {:page-id (:page-id thread) :items [thread]}
|
||||
result))))]
|
||||
(reverse
|
||||
(reduce group-by-page nil threads))))
|
||||
|
||||
(defn- apply-filters
|
||||
[local profile threads]
|
||||
(cond->> threads
|
||||
(true? (:filter-resolved local))
|
||||
(filter (fn [item]
|
||||
(or (not (:is-resolved item))
|
||||
(= (:id item) (:open local)))))
|
||||
|
||||
(= :yours (:filter local))
|
||||
(filter #(contains? (:participants %) (:id profile)))))
|
||||
|
||||
|
|
|
@ -35,3 +35,9 @@
|
|||
(.fillText context letters (/ size 2) (/ size 1.5))
|
||||
|
||||
(.toDataURL canvas)))
|
||||
|
||||
(defn assoc-profile-avatar
|
||||
[{:keys [photo fullname] :as profile}]
|
||||
(cond-> profile
|
||||
(or (nil? photo) (empty? photo))
|
||||
(assoc :photo (generate {:name fullname}))))
|
||||
|
|
|
@ -9,12 +9,11 @@
|
|||
|
||||
(ns app.util.dom
|
||||
(:require
|
||||
[goog.dom :as dom]
|
||||
[cuerdas.core :as str]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.util.transit :as ts]))
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[goog.dom :as dom]))
|
||||
|
||||
;; --- Deprecated methods
|
||||
|
||||
|
@ -111,11 +110,11 @@
|
|||
|
||||
(defn select-text!
|
||||
[node]
|
||||
(.select node))
|
||||
(.select ^js node))
|
||||
|
||||
(defn ^boolean equals?
|
||||
[node-a node-b]
|
||||
(.isEqualNode node-a node-b))
|
||||
(.isEqualNode ^js node-a node-b))
|
||||
|
||||
(defn get-event-files
|
||||
"Extract the files from event instance."
|
||||
|
@ -162,6 +161,12 @@
|
|||
y (.-clientY event)]
|
||||
(gpt/point x y)))
|
||||
|
||||
(defn get-offset-position
|
||||
[event]
|
||||
(let [x (.-offsetX event)
|
||||
y (.-offsetY event)]
|
||||
(gpt/point x y)))
|
||||
|
||||
(defn get-client-size
|
||||
[node]
|
||||
{:width (.-clientWidth ^js node)
|
||||
|
@ -188,7 +193,16 @@
|
|||
|
||||
(defn fullscreen?
|
||||
[]
|
||||
(boolean (.-fullscreenElement js/document)))
|
||||
(cond
|
||||
(obj/in? js/document "webkitFullscreenElement")
|
||||
(boolean (.-webkitFullscreenElement js/document))
|
||||
|
||||
(obj/in? js/document "fullscreenElement")
|
||||
(boolean (.-fullscreenElement js/document))
|
||||
|
||||
:else
|
||||
(ex/raise :type :not-supported
|
||||
:hint "seems like the current browset does not support fullscreen api.")))
|
||||
|
||||
(defn ^boolean blob?
|
||||
[v]
|
||||
|
|
|
@ -76,3 +76,7 @@
|
|||
(defn clj->props
|
||||
[props]
|
||||
(clj->js props :keyword-fn props-key-fn))
|
||||
|
||||
(defn ^boolean in?
|
||||
[obj prop]
|
||||
(js* "~{} in ~{}" prop obj))
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
(ns app.util.webapi
|
||||
"HTML5 web api helpers."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.util.object :as obj]
|
||||
[promesa.core :as p]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -97,8 +99,26 @@
|
|||
|
||||
(defn request-fullscreen
|
||||
[el]
|
||||
(.requestFullscreen el))
|
||||
(cond
|
||||
(obj/in? el "requestFullscreen")
|
||||
(.requestFullscreen el)
|
||||
|
||||
(obj/in? el "webkitRequestFullscreen")
|
||||
(.webkitRequestFullscreen el)
|
||||
|
||||
:else
|
||||
(ex/raise :type :not-supported
|
||||
:hint "seems like the current browset does not support fullscreen api.")))
|
||||
|
||||
(defn exit-fullscreen
|
||||
[]
|
||||
(.exitFullscreen js/document))
|
||||
(cond
|
||||
(obj/in? js/document "exitFullscreen")
|
||||
(.exitFullscreen js/document)
|
||||
|
||||
(obj/in? js/document "webkitExitFullscreen")
|
||||
(.webkitExitFullscreen js/document)
|
||||
|
||||
:else
|
||||
(ex/raise :type :not-supported
|
||||
:hint "seems like the current browset does not support fullscreen api.")))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue