0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-04 13:50:12 -05:00

♻️ Visual changes in comments

This commit is contained in:
luisddm 2024-12-09 17:16:27 +01:00 committed by Alonso Torres
parent ff7acea95a
commit b5e5c4b0dd
17 changed files with 745 additions and 606 deletions

View file

@ -104,8 +104,7 @@ export class ViewerPage extends BaseWebSocketPage {
async showCommentsThread(number, clickOptions = {}) {
await this.page
.getByTestId("floating-thread-bubble")
.filter({ hasText: number.toString() })
.getByTestId(`floating-thread-bubble-${number.toString()}`)
.click(clickOptions);
}

View file

@ -810,28 +810,6 @@
}
}
.comment-bubbles {
@include bodySmallTypography;
@include flexCenter;
height: $s-32;
width: $s-32;
border-radius: $br-circle;
background-color: var(--comment-bullet-background-color-rest);
border: $s-1 solid var(--comment-bullet-border-color-rest);
color: var(--comment-bullet-foreground-color-rest);
}
.resolved-comment-bubble {
background-color: var(--comment-bullet-background-color-resolved);
border: $s-1 solid var(--comment-bullet-border-color-resolved);
color: var(--comment-bullet-foreground-color-resolved);
}
.unread-comment-bubble {
background-color: var(--comment-bullet-background-color-unread);
border: $s-1 solid var(--comment-bullet-border-color-unread);
color: var(--comment-bullet-foreground-color-unread);
}
// SELECTS AND DROPDOWNS
.menu-dropdown {
@include menuShadow;

View file

@ -502,11 +502,11 @@
(d/update-in-when [:comments-local :draft] merge data)))))
(defn toggle-comment-options
[comment]
[comment-id]
(ptk/reify ::toggle-comment-options
ptk/UpdateEvent
(update [_ state]
(update-in state [:comments-local :options] #(if (= (:id comment) %) nil (:id comment))))))
(update-in state [:comments-local :options] #(if (= comment-id %) nil comment-id)))))
(defn hide-comment-options
[]

View file

@ -151,11 +151,13 @@
(pcb/with-page page)
(pcb/set-comment-thread-position thread))]
(rx/merge
(rx/of (dch/commit-changes changes))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore))))))))
(rx/concat
(rx/merge
(rx/of (dch/commit-changes changes))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore)))
(rx/of (dcmt/refresh-comment-thread thread))))))))
;; Move comment threads that are inside a frame when that frame is moved"
(defmethod ptk/resolve ::move-frame-comment-threads

View file

@ -267,9 +267,9 @@
(def workspace-page-flows
(l/derived #(-> % :flows not-empty) workspace-page))
(defn workspace-page-objects-by-id
[page-id]
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
(defn workspace-page-object-by-id
[page-id shape-id]
(l/derived #(wsh/lookup-shape % page-id shape-id) st/state =))
;; TODO: Looks like using the `=` comparator can be pretty expensive
;; on large pages, we are using this for some reason?

View file

@ -9,7 +9,9 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.comments :as dcm]
[app.main.data.modal :as modal]
@ -17,12 +19,15 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.time :as dt]
[clojure.math :refer [floor]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
@ -93,8 +98,66 @@
:on-change on-change*
:max-length max-length}]))
(mf/defc reply-form
[{:keys [thread] :as props}]
(def ^:private schema:comment-avatar
[:map
[:class {:optional true} :string]
[:image :string]
[:variant {:optional true}
[:maybe [:enum "read" "unread" "solved"]]]])
(mf/defc comment-avatar*
{::mf/props :obj
::mf/schema schema:comment-avatar}
[{:keys [image variant class] :rest props}]
(let [variant (or variant "read")
class (dm/str class " " (stl/css-case :avatar true
:avatar-read (= variant "read")
:avatar-unread (= variant "unread")
:avatar-solved (= variant "solved")))
props (mf/spread-props props {:class class})]
[:> :div props
[:img {:src image
:class (stl/css :avatar-image)}]
[:div {:class (stl/css-case :avatar-mask true
:avatar-darken (= variant "solved"))}]]))
(mf/defc comment-info*
{::mf/props :obj
::mf/private true}
[{:keys [item profile]}]
[:*
[:div {:class (stl/css :author)}
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url profile)
:class (stl/css :avatar-lg)
:variant (cond (:is-resolved item) "solved"
(pos? (:count-unread-comments item)) "unread"
:else "read")}]
[:div {:class (stl/css :author-identity)}
[:div {:class (stl/css :author-fullname)} (:fullname profile)]
[:div {:class (stl/css :author-timeago)} (dt/timeago (:modified-at item))]]]
[:div {:class (stl/css :item)}
(:content item)]
[:div {:class (stl/css :replies)}
(let [total-comments (:count-comments item 1)
total-replies (dec total-comments)
unread-replies (:count-unread-comments item 0)]
[:*
(when (> total-replies 0)
(if (= total-replies 1)
[:span {:class (stl/css :replies-total)} (str total-replies " " (tr "labels.reply"))]
[:span {:class (stl/css :replies-total)} (str total-replies " " (tr "labels.replies"))]))
(when (and (> total-replies 0) (> unread-replies 0))
(if (= unread-replies 1)
[:span {:class (stl/css :replies-unread)} (str unread-replies " " (tr "labels.reply.new"))]
[:span {:class (stl/css :replies-unread)} (str unread-replies " " (tr "labels.replies.new"))]))])]])
(mf/defc comment-reply-form*
{::mf/props :obj
::mf/private true}
[{:keys [thread]}]
(let [show-buttons? (mf/use-state false)
content (mf/use-state "")
@ -124,9 +187,10 @@
(fn []
(st/emit! (dcm/add-comment thread @content))
(on-cancel)))]
[:div {:class (stl/css :reply-form)}
[:div {:class (stl/css :form)}
[:& resizing-textarea {:value @content
:placeholder "Reply"
:placeholder (tr "labels.reply.thread")
:autofocus true
:on-blur on-blur
:on-focus on-focus
:select-on-focus? false
@ -134,29 +198,62 @@
:on-change on-change
:max-length 750}]
(when (or @show-buttons? (seq @content))
[:div {:class (stl/css :buttons-wrapper)}
[:input.btn-secondary
{:type "button"
:class (stl/css :cancel-btn)
:value "Cancel"
:on-click on-cancel}]
[:input
{:type "button"
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:value "Post"
:on-click on-submit
:disabled disabled?}]])]))
[:div {:class (stl/css :form-buttons-wrapper)}
[:> button* {:variant "ghost"
:on-click on-cancel}
(tr "ds.confirm-cancel")]
[:> button* {:variant "primary"
:on-click on-submit
:disabled disabled?}
(tr "labels.post")]])]))
(mf/defc draft-thread
(mf/defc comment-edit-form*
{::mf/props :obj
::mf/private true}
[{:keys [content on-submit on-cancel]}]
(let [content (mf/use-state content)
on-change
(mf/use-fn
#(reset! content %))
on-submit*
(mf/use-fn
(mf/deps @content)
(fn [] (on-submit @content)))
disabled? (or (str/blank? @content)
(str/empty? @content))]
[:div {:class (stl/css :form)}
[:& resizing-textarea {:value @content
:autofocus true
:select-on-focus true
:select-on-focus? false
:on-ctrl-enter on-submit*
:on-change on-change
:max-length 750}]
[:div {:class (stl/css :form-buttons-wrapper)}
[:> button* {:variant "ghost"
:on-click on-cancel}
(tr "ds.confirm-cancel")]
[:> button* {:variant "primary"
:on-click on-submit*
:disabled disabled?}
(tr "labels.post")]]]))
(mf/defc comment-floating-thread-draft*
{::mf/props :obj}
[{:keys [draft zoom on-cancel on-submit position-modifier]}]
(let [position (cond-> (:position draft)
(some? position-modifier)
(gpt/transform position-modifier))
content (:content draft)
(let [profile (mf/deref refs/profile)
pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom)
position (cond-> (:position draft)
(some? position-modifier)
(gpt/transform position-modifier))
content (:content draft)
pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom)
disabled? (or (str/blank? content)
(str/empty? content))
@ -183,17 +280,18 @@
[:*
[:div
{:class (stl/css :floating-thread-bubble)
{:class (stl/css :floating-preview-wrapper)
:data-testid "floating-thread-bubble"
:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-click dom/stop-propagation}
"?"]
[:div {:class (stl/css :thread-content)
[:> comment-avatar* {:class (stl/css :avatar-lg)
:image (cfg/resolve-profile-photo-url profile)}]]
[:div {:class (stl/css :floating-thread-wrapper)
:style {:top (str (- pos-y 24) "px")
:left (str (+ pos-x 28) "px")}
:on-click dom/stop-propagation}
[:div {:class (stl/css :reply-form)}
[:div {:class (stl/css :form)}
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
:value (or content "")
:autofocus true
@ -202,58 +300,89 @@
:on-change on-change
:on-ctrl-enter on-submit
:max-length 750}]
[:div {:class (stl/css :buttons-wrapper)}
[:div {:class (stl/css :form-buttons-wrapper)}
[:> button* {:variant "ghost"
:on-click on-esc}
(tr "ds.confirm-cancel")]
[:> button* {:variant "primary"
:on-click on-submit
:disabled disabled?}
(tr "labels.post")]]]]]))
[:input {:on-click on-esc
:class (stl/css :cancel-btn)
:type "button"
:value "Cancel"}]
(mf/defc comment-floating-thread-header*
{::mf/props :obj
::mf/private true}
[{:keys [profiles thread origin]}]
(let [owner (get profiles (:owner-id thread))
profile (mf/deref refs/profile)
options (mf/deref comments-local-options)
[:input {:on-click on-submit
:type "button"
:value "Post"
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:disabled disabled?}]]]]]))
(mf/defc edit-form
[{:keys [content on-submit on-cancel] :as props}]
(let [content (mf/use-state content)
on-change
toggle-resolved
(mf/use-fn
#(reset! content %))
(mf/deps thread)
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))
on-submit*
on-toggle-options
(mf/use-fn
(mf/deps @content)
(fn [] (on-submit @content)))
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/toggle-comment-options uuid/zero))))
disabled? (or (str/blank? @content)
(str/empty? @content))]
delete-thread
(mf/use-fn
(fn []
(st/emit! (dcm/close-thread)
(if (= origin :viewer)
(dcm/delete-comment-thread-on-viewer thread)
(dcm/delete-comment-thread-on-workspace thread)))))
[:div {:class (stl/css :edit-form)}
[:& resizing-textarea {:value @content
:autofocus true
:select-on-focus true
:select-on-focus? false
:on-ctrl-enter on-submit*
:on-change on-change
:max-length 750}]
[:div {:class (stl/css :buttons-wrapper)}
[:input {:type "button"
:value "Cancel"
:class (stl/css :cancel-btn)
:on-click on-cancel}]
[:input {:type "button"
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:value "Post"
:on-click on-submit*
:disabled disabled?}]]]))
on-delete-thread
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/hide-comment-options))
(st/emit! (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}))))
(mf/defc comment-item
[{:keys [comment thread profiles origin] :as props}]
on-hide-options
(mf/use-fn
(mf/deps options)
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/hide-comment-options))))]
[:*
[:div {:class (stl/css :floating-thread-header-left)}
(tr "labels.comment") " " [:span {:class (stl/css :grayed-text)} "#" (:seqn thread)]]
[:div {:class (stl/css :floating-thread-header-right)}
(when (some? thread)
[:div {:class (stl/css :checkbox-wrapper)
:title (tr "labels.comment.mark-as-solved")
:on-click toggle-resolved}
[:span {:class (stl/css-case :checkbox true
:global/checked (:is-resolved thread))} i/tick]])
(when (= (:id profile) (:id owner))
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.options")
:on-click on-toggle-options
:icon "menu"}])]
[:& dropdown {:show (= options uuid/zero)
:on-close on-hide-options}
[:ul {:class (stl/css :dropdown-menu)}
[:li {:class (stl/css :dropdown-menu-option)
:on-click on-delete-thread}
(tr "labels.delete-comment-thread")]]]]))
(mf/defc comment-floating-thread-item*
{::mf/props :obj
::mf/private true}
[{:keys [comment thread profiles]}]
(let [owner (get profiles (:owner-id comment))
profile (mf/deref refs/profile)
options (mf/deref comments-local-options)
@ -264,7 +393,7 @@
(mf/deps options)
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/toggle-comment-options comment))))
(st/emit! (dcm/toggle-comment-options (:id comment)))))
on-hide-options
(mf/use-fn
@ -285,24 +414,6 @@
(mf/deps comment)
#(st/emit! (dcm/delete-comment comment)))
delete-thread
(mf/use-fn
(mf/deps thread)
#(st/emit! (dcm/close-thread)
(if (= origin :viewer)
(dcm/delete-comment-thread-on-viewer thread)
(dcm/delete-comment-thread-on-workspace thread))))
on-delete-thread
(mf/use-fn
(mf/deps thread)
#(st/emit! (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-fn
(mf/deps comment thread)
@ -311,29 +422,15 @@
(st/emit! (dcm/update-comment (assoc comment :content content)))))
on-cancel
(mf/use-fn #(reset! edition? false))
(mf/use-fn #(reset! edition? false))]
toggle-resolved
(mf/use-fn
(mf/deps thread)
(fn [event]
(dom/stop-propagation event)
(st/emit! (dcm/update-comment-thread (update thread :is-resolved not)))))]
[:div {:class (stl/css :comment-container)}
[:div {:class (stl/css :comment)}
[:div {:class (stl/css :floating-thread-item-wrapper)}
[:div {:class (stl/css :floating-thread-item)}
[:div {:class (stl/css :author)}
[:div {:class (stl/css :avatar)}
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div {:class (stl/css :name)}
[:div {:class (stl/css :fullname)} (:fullname owner)]
[:div {:class (stl/css :timeago)} (dt/timeago (:modified-at comment))]]
(when (some? thread)
[:div {:class (stl/css :options-resolve-wrapper)
:on-click toggle-resolved}
[:span {:class (stl/css-case :options-resolve true
:global/checked (:is-resolved thread))} i/tick]])
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url owner)}]
[:div {:class (stl/css :author-identity)}
[:div {:class (stl/css :author-fullname)} (:fullname owner)]
[:div {:class (stl/css :author-timeago)} (dt/timeago (:modified-at comment))]]
(when (= (:id profile) (:id owner))
[:> icon-button* {:variant "ghost"
@ -341,24 +438,21 @@
:on-click on-toggle-options
:icon "menu"}])]
[:div {:class (stl/css :content)}
[:div {:class (stl/css :item)}
(if @edition?
[:& edit-form {:content (:content comment)
:on-submit on-submit
:on-cancel on-cancel}]
[:> comment-edit-form* {:content (:content comment)
:on-submit on-submit
:on-cancel on-cancel}]
[:span {:class (stl/css :text)} (:content comment)])]]
[:& dropdown {:show (= options (:id comment))
:on-close on-hide-options}
[:ul {:class (stl/css :comment-options-dropdown)}
[:li {:class (stl/css :context-menu-option)
[:ul {:class (stl/css :dropdown-menu)}
[:li {:class (stl/css :dropdown-menu-option)
:on-click on-edit-clicked}
(tr "labels.edit")]
(if thread
[:li {:class (stl/css :context-menu-option)
:on-click on-delete-thread}
(tr "labels.delete-comment-thread")]
[:li {:class (stl/css :context-menu-option)
(when-not thread
[:li {:class (stl/css :dropdown-menu-option)
:on-click on-delete-comment}
(tr "labels.delete-comment")])]]]))
@ -370,48 +464,54 @@
(let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0})
base-x (+ (* (:x position) zoom) (:offset-x viewport))
base-y (+ (* (:y position) zoom) (:offset-y viewport))
x (:x position)
y (:y position)
w (:width viewport)
h (:height viewport)
comment-width 284 ;; TODO: this is the width set via CSS in an outer container…
;; We should probably do this in a different way.
orientation-left? (>= (+ base-x comment-width (:x bubble-margin)) w)
orientation-top? (>= base-y (/ h 2))
orientation-top? (>= base-y (/ h 2))
h-dir (if orientation-left? :left :right)
v-dir (if orientation-top? :top :bottom)
x (:x position)
y (:y position)]
v-dir (if orientation-top? :top :bottom)]
{:x x :y y :h-dir h-dir :v-dir v-dir}))
(mf/defc thread-comments
{::mf/wrap [mf/memo]}
(mf/defc comment-floating-thread*
{::mf/props :obj
::mf/wrap [mf/memo]}
[{:keys [thread zoom profiles origin position-modifier viewport]}]
(let [ref (mf/use-ref)
thread-id (:id thread)
thread-pos (:position thread)
(let [ref (mf/use-ref)
thread-id (:id thread)
thread-pos (:position thread)
base-pos (cond-> thread-pos
(some? position-modifier)
(gpt/transform position-modifier))
base-pos (cond-> thread-pos
(some? position-modifier)
(gpt/transform position-modifier))
max-height (when (some? viewport) (int (* (:height viewport) 0.75)))
max-height (when (some? viewport) (int (* (:height viewport) 0.75)))
;; We should probably look for a better way of doing this.
bubble-margin {:x 24 :y 0}
pos (offset-position base-pos viewport zoom bubble-margin)
bubble-margin {:x 24 :y 24}
pos (offset-position base-pos viewport zoom bubble-margin)
margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1))
margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1))
pos-x (+ (* (:x pos) zoom) margin-x)
pos-y (- (* (:y pos) zoom) margin-y)
margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1))
margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1))
pos-x (+ (* (:x pos) zoom) margin-x)
pos-y (- (* (:y pos) zoom) margin-y)
comments-ref (mf/with-memo [thread-id]
(make-comments-ref thread-id))
comments-map (mf/deref comments-ref)
comments-ref (mf/with-memo [thread-id]
(make-comments-ref thread-id))
comments-map (mf/deref comments-ref)
comments (mf/with-memo [comments-map]
(->> (vals comments-map)
(sort-by :created-at)))
comments (mf/with-memo [comments-map]
(->> (vals comments-map)
(sort-by :created-at)))
comment (first comments)]
first-comment (first comments)]
(mf/with-effect [thread-id]
(st/emit! (dcm/retrieve-comments thread-id)))
@ -423,158 +523,170 @@
(when-let [node (mf/ref-val ref)]
(dom/scroll-into-view-if-needed! node)))
(when (some? comment)
[:div {:class (stl/css-case :thread-content true
:thread-content-left (= (:h-dir pos) :left)
:thread-content-top (= (:v-dir pos) :top))
(when (some? first-comment)
[:div {:class (stl/css-case :floating-thread-wrapper true
:left (= (:h-dir pos) :left)
:top (= (:v-dir pos) :top))
:id (str "thread-" thread-id)
:style {:left (str pos-x "px")
:top (str pos-y "px")
:max-height max-height}
:on-click dom/stop-propagation}
[:div {:class (stl/css :comments)}
[:& comment-item {:comment comment
:profiles profiles
:thread thread
:origin origin}]
[:div {:class (stl/css :floating-thread-header)}
[:> comment-floating-thread-header* {:profiles profiles
:thread thread
:origin origin}]]
[:div {:class (stl/css :floating-thread-main)}
[:> comment-floating-thread-item* {:comment first-comment
:profiles profiles
:thread thread}]
(for [item (rest comments)]
[:* {:key (dm/str (:id item))}
[:& comment-item {:comment item
:profiles profiles
:origin origin}]])]
[:& reply-form {:thread thread}]
[:div {:ref ref}]])))
[:> comment-floating-thread-item* {:comment item
:profiles profiles}]])]
(defn use-buble
[zoom {:keys [position frame-id]}]
(let [dragging-ref (mf/use-ref false)
[:> comment-reply-form* {:thread thread}]])))
(mf/defc comment-floating-bubble*
{::mf/props :obj
::mf/wrap [mf/memo]}
[{:keys [thread profiles zoom is-open on-click origin position-modifier]}]
(let [owner (get profiles (:owner-id thread))
base-pos (cond-> (:position thread)
(some? position-modifier)
(gpt/transform position-modifier))
drag? (mf/use-ref nil)
was-open? (mf/use-ref nil)
dragging-ref (mf/use-ref false)
start-ref (mf/use-ref nil)
state (mf/use-state {:hover false
position (:position thread)
frame-id (:frame-id thread)
state (mf/use-state {:hover? false
:grabbing? false
:new-position-x nil
:new-position-y nil
:new-frame-id frame-id})
pos-x (floor (* (or (:new-position-x @state) (:x base-pos)) zoom))
pos-y (floor (* (or (:new-position-y @state) (:y base-pos)) zoom))
on-pointer-down
(mf/use-fn
(mf/deps origin was-open? is-open drag?)
(fn [event]
(dom/capture-pointer event)
(mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-ref (dom/get-client-position event))))
(when (not= origin :viewer)
(swap! state assoc :grabbing? true)
(mf/set-ref-val! was-open? is-open)
(when is-open (st/emit! (dcm/close-thread)))
(mf/set-ref-val! drag? false)
(dom/stop-propagation event)
(dom/capture-pointer event)
(mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-ref (dom/get-client-position event)))))
on-pointer-up
(mf/use-fn
(mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
(fn [_ thread]
(when (and
(some? (:new-position-x @state))
(some? (:new-position-y @state)))
(st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state) (:new-position-y @state)])))))
on-lost-pointer-capture
(mf/use-fn
(mf/deps origin thread (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
(fn [event]
(dom/release-pointer event)
(mf/set-ref-val! dragging-ref false)
(mf/set-ref-val! start-ref nil)
(swap! state assoc :new-position-x nil)
(swap! state assoc :new-position-y nil)))
(when (not= origin :viewer)
(swap! state assoc :grabbing? false)
(dom/stop-propagation event)
(dom/release-pointer event)
(mf/set-ref-val! dragging-ref false)
(mf/set-ref-val! start-ref nil)
(when (and
(some? (:new-position-x @state))
(some? (:new-position-y @state)))
(st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state)
(:new-position-y @state)]))
(swap! state assoc
:new-position-x nil
:new-position-y nil)))))
on-pointer-move
(mf/use-fn
(mf/deps position zoom)
(fn [event]
(when-let [_ (mf/ref-val dragging-ref)]
(let [start-pt (mf/ref-val start-ref)
current-pt (dom/get-client-position event)
delta-x (/ (- (:x current-pt) (:x start-pt)) zoom)
delta-y (/ (- (:y current-pt) (:y start-pt)) zoom)]
(swap! state assoc
:new-position-x (+ (:x position) delta-x)
:new-position-y (+ (:y position) delta-y))))))]
{:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-pointer-move on-pointer-move
:on-lost-pointer-capture on-lost-pointer-capture
:state state}))
(mf/defc thread-bubble
{::mf/wrap [mf/memo]}
[{:keys [thread zoom open? on-click origin position-modifier]}]
(let [pos (cond-> (:position thread)
(some? position-modifier)
(gpt/transform position-modifier))
drag? (mf/use-ref nil)
was-open? (mf/use-ref nil)
{:keys [on-pointer-down
on-pointer-up
on-pointer-move
state
on-lost-pointer-capture]} (use-buble zoom thread)
pos-x (* (or (:new-position-x @state) (:x pos)) zoom)
pos-y (* (or (:new-position-y @state) (:y pos)) zoom)
on-pointer-down*
(mf/use-fn
(mf/deps origin was-open? open? drag? on-pointer-down)
(fn [event]
(when (not= origin :viewer)
(mf/set-ref-val! was-open? open?)
(when open? (st/emit! (dcm/close-thread)))
(mf/set-ref-val! drag? false)
(dom/stop-propagation event)
(on-pointer-down event))))
on-pointer-up*
(mf/use-fn
(mf/deps origin thread was-open? drag? on-pointer-up)
(fn [event]
(when (not= origin :viewer)
(dom/stop-propagation event)
(on-pointer-up event thread)
(when (or (and (mf/ref-val was-open?) (mf/ref-val drag?))
(and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?))))
(st/emit! (dcm/open-thread thread))))))
on-pointer-move*
(mf/use-fn
(mf/deps origin drag? on-pointer-move)
(mf/deps origin drag? position zoom)
(fn [event]
(when (not= origin :viewer)
(mf/set-ref-val! drag? true)
(dom/stop-propagation event)
(on-pointer-move event))))
(when-let [_ (mf/ref-val dragging-ref)]
(let [start-pt (mf/ref-val start-ref)
current-pt (dom/get-client-position event)
delta-x (/ (- (:x current-pt) (:x start-pt)) zoom)
delta-y (/ (- (:y current-pt) (:y start-pt)) zoom)]
(swap! state assoc
:new-position-x (+ (:x position) delta-x)
:new-position-y (+ (:y position) delta-y)))))))
on-pointer-enter
(mf/use-fn
(mf/deps is-open)
(fn [event]
(dom/stop-propagation event)
(when (false? is-open)
(swap! state assoc :hover? true))))
on-pointer-leave
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(swap! state assoc :hover? false)))
on-click*
(mf/use-fn
(mf/deps origin thread on-click)
(mf/deps origin thread on-click was-open? drag? (select-keys @state [:hover?]))
(fn [event]
(dom/stop-propagation event)
(when (or (and (mf/ref-val was-open?) (mf/ref-val drag?))
(and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?))))
(swap! state assoc :hover? false)
(st/emit! (dcm/open-thread thread)))
(when (= origin :viewer)
(on-click thread))))]
[:div {:style {:top (str pos-y "px")
:left (str pos-x "px")}
:on-pointer-down on-pointer-down*
:on-pointer-up on-pointer-up*
:on-pointer-move on-pointer-move*
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up
:on-pointer-move on-pointer-move
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-click on-click*
:on-lost-pointer-capture on-lost-pointer-capture
:data-testid "floating-thread-bubble"
:class (stl/css-case
:floating-thread-bubble true
:resolved (:is-resolved thread)
:unread (pos? (:count-unread-comments thread)))}
[:span (:seqn thread)]]))
:class (stl/css-case :floating-preview-wrapper true
:floating-preview-bubble (false? (:hover? @state))
:grabbing (true? (:grabbing? @state)))}
(mf/defc comment-thread
(if (true? (:hover? @state))
[:div {:class (stl/css :floating-thread-wrapper :floating-preview-displacement)}
[:div {:class (stl/css :floating-thread-item-wrapper)}
[:div {:class (stl/css :floating-thread-item)}
[:> comment-info* {:item thread
:profile owner}]]]]
[:> comment-avatar* {:image (cfg/resolve-profile-photo-url owner)
:class (stl/css :avatar-lg)
:data-testid (str "floating-thread-bubble-" (:seqn thread))
:variant (cond (:is-resolved thread) "solved"
(pos? (:count-unread-comments thread)) "unread"
:else "read")}])]))
(mf/defc comment-sidebar-thread-item*
{::mf/props :obj
::mf/private true}
[{:keys [item profiles on-click]}]
(let [owner (get profiles (:owner-id item))
frame (mf/deref (refs/workspace-page-object-by-id (:page-id item) (:frame-id item)))
on-click*
(mf/use-fn
(mf/deps item)
@ -584,52 +696,64 @@
(when (fn? on-click)
(on-click item))))]
[:div {:class (stl/css :comment)
[:div {:class (stl/css :cover)
:on-click on-click*}
[:div {:class (stl/css :author)}
[:div {:class (stl/css-case :thread-bubble true
:resolved (:is-resolved item)
:unread (pos? (:count-unread-comments item)))}
(:seqn item)]
[:div {:class (stl/css :avatar)}
[:img {:src (cfg/resolve-profile-photo-url owner)}]]
[:div {:class (stl/css :name)}
[:div {:class (stl/css :fullname)} (:fullname owner)]
[:div {:class (stl/css :timeago)} (dt/timeago (:modified-at item))]]]
[:div {:class (stl/css :content)}
(:content item)]
[:div {:class (stl/css :replies)}
(let [unread (:count-unread-comments item ::none)
total (:count-comments item 1)]
[:*
(when (> total 1)
(if (= total 2)
[:span {:class (stl/css :total-replies)} "1 reply"]
[:span {:class (stl/css :total-replies)} (str (dec total) " replies")]))
[:div {:class (stl/css :location)}
[:div {:class (stl/css :location-text)}
(str "#" (:seqn item))
(str " - " (:page-name item))
(when (and (some? frame) (not (cfh/root? frame)))
(str " - " (:name frame)))]]
(when (and (> total 1) (> unread 0))
(if (= unread 1)
[:span {:class (stl/css :new-replies)} "1 new reply"]
[:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]))
[:> comment-info* {:item item
:profile owner}]]))
(mf/defc comment-thread-group
(mf/defc comment-sidebar-thread-group*
{::mf/props :obj}
[{:keys [group profiles on-thread-click]}]
[:div {:class (stl/css :thread-group)}
(if (:file-name group)
[:div {:class (stl/css :section-title)
:title (str (:file-name group) ", " (:page-name group))}
[:span {:class (stl/css :file-name)} (:file-name group) ", "]
[:span {:class (stl/css :page-name)} (:page-name group)]]
[:div
(for [item (:items group)]
[:> comment-sidebar-thread-item*
{:item item
:on-click on-thread-click
:profiles profiles
:key (:id item)}])])
[:div {:class (stl/css :section-title)
:title (:page-name group)}
[:span {:class (stl/css :icon)} i/document]
[:span {:class (stl/css :page-name)} (:page-name group)]])
(mf/defc comment-dashboard-thread-item*
{::mf/props :obj
::mf/private true}
[{:keys [item profiles on-click]}]
(let [owner (get profiles (:owner-id item))
[:div {:class (stl/css :threads)}
(for [item (:items group)]
[:& comment-thread
{:item item
:on-click on-thread-click
:profiles profiles
:key (:id item)}])]])
on-click*
(mf/use-fn
(mf/deps item)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when (fn? on-click)
(on-click item))))]
[:div {:class (stl/css :cover)
:on-click on-click*}
[:div {:class (stl/css :location)}
[:> icon* {:id "comments"
:class (stl/css :location-icon)}]
[:div {:class (stl/css :location-text)}
(str "#" (:seqn item))
(str " " (:file-name item))
(str ", " (:page-name item))]]
[:> comment-info* {:item item
:profile owner}]]))
(mf/defc comment-dashboard-thread-group*
{::mf/props :obj}
[{:keys [group profiles on-thread-click]}]
[:div
(for [item (:items group)]
[:> comment-dashboard-thread-item*
{:item item
:on-click on-thread-click
:profiles profiles
:key (:id item)}])])

View file

@ -6,103 +6,98 @@
@import "refactor/common-refactor.scss";
// Comment-thread-group
.thread-group {
padding: 0 $s-12;
cursor: pointer;
border-radius: $br-8;
padding: $s-8 $s-16;
&:hover {
background: var(--comment-thread-background-color-hover);
}
}
.section-title {
display: grid;
grid-template-columns: auto auto;
@include bodySmallTypography;
height: $s-32;
display: flex;
align-items: center;
margin-bottom: $s-8;
}
.file-name {
@include textEllipsis;
.grayed-text {
color: var(--comment-subtitle-color);
}
.page-name {
@include textEllipsis;
.location {
color: var(--comment-subtitle-color);
}
.icon {
display: flex;
align-items: center;
padding: 0 $s-6 0 $s-4;
width: $s-24;
height: $s-32;
margin-left: $s-6;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.threads {
display: flex;
flex-direction: column;
gap: $s-24;
}
// Comment-thread
.comment {
@include bodySmallTypography;
display: flex;
flex-direction: column;
gap: $s-12;
}
.author {
display: flex;
gap: $s-8;
}
.thread-bubble {
@extend .comment-bubbles;
&.resolved {
@extend .resolved-comment-bubble;
}
&.unread {
@extend .unread-comment-bubble;
}
.location-icon {
display: flex;
}
.location-text {
@include textEllipsis;
}
.author {
@include bodySmallTypography;
display: flex;
align-items: center;
gap: $s-8;
}
.author-identity {
flex-grow: 1;
}
.author-fullname {
@include textEllipsis;
color: var(--comment-title-color);
}
.author-timeago {
@include textEllipsis;
color: var(--comment-subtitle-color);
}
.avatar {
position: relative;
height: $s-24;
width: $s-24;
border-radius: $br-circle;
}
.avatar-lg {
height: $s-32;
width: $s-32;
}
.avatar-read {
border: $s-2 solid var(--color-background-tertiary);
}
.avatar-unread {
border: $s-2 solid var(--color-accent-primary);
}
.avatar-solved {
border: $s-2 solid var(--color-background-tertiary);
}
.avatar-image {
border-radius: $br-circle;
img {
border-radius: $br-circle;
}
}
.name {
flex-grow: 1;
.fullname {
@include textEllipsis;
color: var(--comment-title-color);
}
.timeago {
@include textEllipsis;
color: var(--comment-subtitle-color);
}
.avatar-mask {
border-radius: $br-circle;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
.content {
position: relative;
.avatar-darken {
background: rgba(0, 0, 0, 0.5);
}
.cover {
@include bodySmallTypography;
cursor: pointer;
display: flex;
flex-direction: column;
gap: $s-8;
padding: $s-20;
border-bottom: $s-1 solid var(--color-background-quaternary);
}
.item {
@include bodySmallTypography;
color: var(--color-foreground-primary);
word-wrap: break-word;
@ -112,119 +107,128 @@
}
.replies {
@include bodySmallTypography;
display: flex;
gap: $s-8;
}
.total-replies {
.replies-total {
color: var(--color-foreground-secondary);
}
.new-replies {
.replies-unread {
color: var(--color-accent-primary);
}
// Thread-bubble
.floating-thread-bubble {
@extend .comment-bubbles;
.floating-preview-wrapper {
z-index: $z-index-1;
position: absolute;
user-select: none;
cursor: pointer;
pointer-events: auto;
transform: translate(calc(-1 * $s-16), calc(-1 * $s-16));
&.resolved {
@extend .resolved-comment-bubble;
}
&.unread {
@extend .unread-comment-bubble;
}
}
// thread-content
.thread-content {
position: absolute;
overflow-y: auto;
width: $s-284;
padding: $s-12;
padding-inline-end: $s-8;
.floating-preview-bubble {
z-index: initial;
}
.floating-preview-displacement {
margin-left: calc(-1 * ($s-12 + $s-2));
margin-top: calc(-1 * ($s-8 + $s-2));
}
.grabbing {
cursor: grabbing;
}
.floating-thread-wrapper {
position: absolute;
display: flex;
flex-direction: column;
gap: $s-12;
width: $s-284;
padding: $s-8 $s-12 $s-8 $s-12;
pointer-events: auto;
user-select: text;
border-radius: $br-8;
border: $s-2 solid var(--modal-border-color);
background-color: var(--comment-modal-background-color);
--translate-x: 0%;
--translate-y: 0%;
transform: translate(var(--translate-x), var(--translate-y));
.comments {
display: flex;
flex-direction: column;
gap: $s-24;
&.left {
--translate-x: -100%;
}
&.top {
--translate-y: -100%;
}
}
.thread-content-left {
--translate-x: -100%;
}
.thread-content-top {
--translate-y: -100%;
}
// comment-item
.comment-container {
.floating-thread-header {
position: relative;
.comment {
@include bodySmallTypography;
.author {
display: flex;
gap: $s-8;
.avatar {
height: $s-32;
width: $s-32;
border-radius: $br-circle;
img {
border-radius: $br-circle;
}
}
.name {
flex-grow: 1;
.fullname {
@include textEllipsis;
color: var(--comment-title-color);
}
.timeago {
@include textEllipsis;
color: var(--comment-subtitle-color);
}
}
.options-resolve-wrapper {
@include flexCenter;
width: $s-16;
height: $s-32;
.options-resolve {
@extend .checkbox-icon;
cursor: pointer;
}
}
}
}
.comment-options-dropdown {
@extend .dropdown-wrapper;
position: absolute;
width: fit-content;
max-width: $s-200;
right: 0;
left: unset;
.context-menu-option {
@extend .dropdown-element-base;
}
}
display: flex;
justify-content: space-between;
align-items: center;
height: $s-32;
}
// edit-form & reply-form
.floating-thread-header-left {
@include bodySmallTypography;
color: var(--color-foreground-primary);
}
.edit-form,
.reply-form {
.floating-thread-header-right {
display: flex;
align-items: center;
}
.floating-thread-main {
display: flex;
flex-direction: column;
gap: $s-16;
overflow-y: auto;
padding-bottom: $s-16;
}
.floating-thread-item-wrapper {
position: relative;
}
.floating-thread-item {
display: flex;
flex-direction: column;
gap: $s-8;
@include bodySmallTypography;
}
.checkbox-wrapper {
@include flexCenter;
width: $s-16;
height: $s-24;
margin-right: $s-8;
}
.checkbox {
@extend .checkbox-icon;
}
.dropdown-menu {
@extend .dropdown-wrapper;
position: absolute;
width: fit-content;
max-width: $s-200;
right: $s-32;
top: 0;
left: unset;
}
.dropdown-menu-option {
@extend .dropdown-element-base;
}
.form {
display: flex;
flex-direction: column;
gap: $s-8;
textarea {
@extend .input-element;
@include bodySmallTypography;
@ -232,8 +236,8 @@
height: 100%;
width: 100%;
max-width: $s-260;
margin-bottom: $s-8;
padding: $s-8;
margin-top: $s-4;
color: var(--input-foreground-color-active);
resize: vertical;
&:focus {
@ -241,21 +245,10 @@
outline: none;
}
}
.buttons-wrapper {
display: flex;
justify-content: flex-end;
gap: $s-4;
.post-btn {
@extend .button-primary;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
.cancel-btn {
@extend .button-secondary;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
}
}
.form-buttons-wrapper {
display: flex;
justify-content: flex-end;
gap: $s-8;
}

View file

@ -14,25 +14,18 @@
[app.main.store :as st]
[app.main.ui.comments :as cmt]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(def ^:private close-icon
(i/icon-xref :close (stl/css :close-icon)))
(def ^:private comments-icon-svg
(i/icon-xref :comments (stl/css :comments-icon)))
(def ^:private comments-icon-small
(i/icon-xref :comments (stl/css :comments-icon-small)))
(mf/defc comments-icon
[{:keys [profile show? on-show-comments]}]
(mf/defc comments-icon*
{::mf/props :obj}
[{:keys [profile on-show-comments]}]
(let [threads-map (mf/deref refs/comment-threads)
@ -41,24 +34,18 @@
(sort-by :modified-at)
(reverse)
(dcm/apply-filters {} profile)
(dcm/group-threads-by-file-and-page))
handle-keydown
(mf/use-callback
(mf/deps on-show-comments)
(fn [event]
(when (kbd/enter? event)
(on-show-comments event))))]
(dcm/group-threads-by-file-and-page))]
[:div {:class (stl/css :dashboard-comments-section)}
[:button {:tab-index "0"
:on-click on-show-comments
:on-key-down handle-keydown
:data-testid "open-comments"
:class (stl/css-case :comment-button true
:open show?
:unread (boolean (seq tgroups)))}
comments-icon-small]]))
[:> icon-button* {:variant "ghost"
:tab-index "0"
:class (stl/css :comment-button)
:data-testid "open-comments"
:aria-label (tr "dashboard.notifications.view")
:on-click on-show-comments
:icon "comments"}
(when (seq tgroups)
[:div {:class (stl/css :unread)}])]]))
(mf/defc comments-section
[{:keys [profile team show? on-hide-comments]}]
@ -72,13 +59,6 @@
(dcm/apply-filters {} profile)
(dcm/group-threads-by-file-and-page))
handle-keydown
(mf/use-callback
(mf/deps on-hide-comments)
(fn [event]
(when (kbd/enter? event)
(on-hide-comments event))))
on-navigate
(mf/use-callback
(fn [thread]
@ -101,22 +81,22 @@
[:& dropdown {:show show? :on-close on-hide-comments}
[:div {:class (stl/css :dropdown :comments-section :comment-threads-section)}
[:div {:class (stl/css :header)}
[:h3 {:class (stl/css :header-title)} (tr "labels.comments")]
[:button {:class (stl/css :close-btn)
:tab-index (if show? "0" "-1")
:on-click on-hide-comments
:on-key-down handle-keydown}
close-icon]]
[:h3 {:class (stl/css :header-title)} (tr "dashboard.notifications")]
[:> icon-button* {:variant "ghost"
:tab-index (if show? "0" "-1")
:aria-label (tr "labels.close")
:on-click on-hide-comments
:icon "close"}]]
(if (seq tgroups)
[:div {:class (stl/css :thread-groups)}
[:& cmt/comment-thread-group
[:> cmt/comment-dashboard-thread-group*
{:group (first tgroups)
:on-thread-click on-navigate
:show-file-name true
:profiles profiles}]
(for [tgroup (rest tgroups)]
[:& cmt/comment-thread-group
[:> cmt/comment-dashboard-thread-group*
{:group tgroup
:on-thread-click on-navigate
:show-file-name true

View file

@ -44,21 +44,16 @@
}
.comment-button {
@include buttonStyle;
@include flexCenter;
border-radius: $br-8;
height: $s-32;
width: $s-32;
--comment-icon-small-foreground-color: var(--icon-foreground);
&.unread,
&.open {
--comment-icon-small-foreground-color: var(--icon-foreground-selected);
}
&:hover {
background-color: var(--color-background-quaternary);
--comment-icon-small-foreground-color: var(--icon-foreground-active);
position: relative;
.unread {
position: absolute;
width: $s-8;
height: $s-8;
border: $s-2 solid var(--color-background-tertiary);
border-radius: 50%;
background: red;
top: $s-6;
right: $s-6;
}
}
@ -100,13 +95,3 @@
flex-grow: 1;
text-transform: uppercase;
}
.close-btn {
@include buttonStyle;
@include flexCenter;
}
.close-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}

View file

@ -23,7 +23,7 @@
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]]
[app.main.ui.dashboard.comments :refer [comments-icon comments-section]]
[app.main.ui.dashboard.comments :refer [comments-icon* comments-section]]
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.team-form]
@ -1071,9 +1071,8 @@
(tr "labels.logout")]]
(when (and team profile)
[:& comments-icon
[:> comments-icon*
{:profile profile
:show? show-comments?
:on-show-comments handle-show-comments}])]]))
(mf/defc sidebar*

View file

@ -206,25 +206,26 @@
[:div {:class (stl/css :viewer-comments-container)}
[:div {:class (stl/css :threads)}
(for [item threads]
[:& cmt/thread-bubble
[:> cmt/comment-floating-bubble*
{:thread item
:profiles users
:position-modifier modifier1
:zoom zoom
:on-click on-bubble-click
:open? (= (:id item) (:open local))
:is-open (= (:id item) (:open local))
:key (:seqn item)
:origin :viewer}])
(when-let [thread (get threads-map open-thread-id)]
[:& cmt/thread-comments
[:> cmt/comment-floating-thread*
{:thread thread
:profiles users
:position-modifier modifier1
:viewport {:offset-x 0 :offset-y 0 :width (:width vsize) :height (:height vsize)}
:profiles users
:zoom zoom}])
(when-let [draft (:draft local)]
[:& cmt/draft-thread
[:> cmt/comment-floating-thread-draft*
{:draft draft
:position-modifier modifier1
:on-cancel on-draft-cancel

View file

@ -150,12 +150,12 @@
(if (seq tgroups)
[:div {:class (stl/css :thread-groups)}
[:& cmt/comment-thread-group
[:> cmt/comment-sidebar-thread-group*
{:group (first tgroups)
:on-thread-click on-thread-click
:profiles profiles}]
(for [tgroup (rest tgroups)]
[:& cmt/comment-thread-group
[:> cmt/comment-sidebar-thread-group*
{:group tgroup
:on-thread-click on-thread-click
:profiles profiles

View file

@ -125,7 +125,6 @@
.thread-groups {
display: flex;
flex-direction: column;
gap: $s-24;
}
.thread-group-placeholder {

View file

@ -211,7 +211,7 @@
show-outlines? (and (nil? transform)
(not edition)
(not drawing-obj)
(not (#{:comments :path :curve} drawing-tool)))
(not (#{:path :curve} drawing-tool)))
show-pixel-grid? (and (contains? layout :show-pixel-grid)
(>= zoom 8))

View file

@ -75,22 +75,23 @@
[:div {:class (stl/css :threads)
:style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}}
(for [item threads]
[:& cmt/thread-bubble {:thread item
:zoom zoom
:open? (= (:id item) (:open local))
:key (:seqn item)}])
[:> cmt/comment-floating-bubble* {:thread item
:profiles profiles
:zoom zoom
:is-open (= (:id item) (:open local))
:key (:seqn item)}])
(when-let [id (:open local)]
(when-let [thread (get threads-map id)]
(when (seq (dcm/apply-filters local profile [thread]))
[:& cmt/thread-comments {:thread (update-position positions thread)
:profiles profiles
:viewport {:offset-x pos-x :offset-y pos-y :width (:width vport) :height (:height vport)}
:zoom zoom}])))
[:> cmt/comment-floating-thread* {:thread (update-position positions thread)
:profiles profiles
:viewport {:offset-x pos-x :offset-y pos-y :width (:width vport) :height (:height vport)}
:zoom zoom}])))
(when-let [draft (:comment drawing)]
[:& cmt/draft-thread {:draft draft
:on-cancel on-draft-cancel
:on-submit on-draft-submit
:zoom zoom}])]]]))
[:> cmt/comment-floating-thread-draft* {:draft draft
:on-cancel on-draft-cancel
:on-submit on-draft-submit
:zoom zoom}])]]]))

View file

@ -748,6 +748,14 @@ msgstr "No matches found for “%s“"
msgid "dashboard.no-projects-placeholder"
msgstr "Pinned projects will appear here"
#: src/app/main/ui/dashboard/comments.cljs
msgid "dashboard.notifications"
msgstr "Notifications"
#: src/app/main/ui/dashboard/comments.cljs
msgid "dashboard.notifications.view"
msgstr "View notifications"
#: src/app/main/ui/auth/verify_token.cljs:32
msgid "dashboard.notifications.email-changed-successfully"
msgstr "Your email address has been updated successfully"
@ -1629,6 +1637,13 @@ msgstr "Close"
msgid "labels.collapse"
msgstr "Collapse"
msgid "labels.comment"
msgstr "Comment"
#: src/app/main/ui/comments.cljs:446
msgid "labels.comment.mark-as-solved"
msgstr "Mark as solved"
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:127
msgid "labels.comments"
msgstr "Comments"
@ -1952,6 +1967,10 @@ msgstr "Password"
msgid "labels.pending-invitation"
msgstr "Pending"
#: src/app/main/ui/comments.cljs:147
msgid "labels.post"
msgstr "Post"
#: src/app/main/ui/onboarding/questions.cljs:51
msgid "labels.previous"
msgstr "Previous"
@ -1998,6 +2017,26 @@ msgstr "Rename"
msgid "labels.rename-team"
msgstr "Rename team"
#: src/app/main/ui/comments.cljs:145
msgid "labels.reply"
msgstr "reply"
#: src/app/main/ui/comments.cljs:150
msgid "labels.reply.new"
msgstr "new reply"
#: src/app/main/ui/comments.cljs:188
msgid "labels.reply.thread"
msgstr "Reply"
#: src/app/main/ui/comments.cljs:146
msgid "labels.replies"
msgstr "replies"
#: src/app/main/ui/comments.cljs:151
msgid "labels.replies.new"
msgstr "new replies"
#: src/app/main/ui/dashboard/team.cljs:681
msgid "labels.resend-invitation"
msgstr "Resend invitation"

View file

@ -759,6 +759,14 @@ msgstr "No se encuentra “%s“"
msgid "dashboard.no-projects-placeholder"
msgstr "Los proyectos fijados aparecerán aquí"
#: src/app/main/ui/dashboard/comments.cljs
msgid "dashboard.notifications"
msgstr "Notificaciones"
#: src/app/main/ui/dashboard/comments.cljs
msgid "dashboard.notifications.view"
msgstr "Ver notificaciones"
#: src/app/main/ui/auth/verify_token.cljs:32
msgid "dashboard.notifications.email-changed-successfully"
msgstr "Tu dirección de correo ha sido actualizada"
@ -1634,6 +1642,13 @@ msgstr "Cerrar"
msgid "labels.collapse"
msgstr "Colapsar"
msgid "labels.comment"
msgstr "Comentario"
#: src/app/main/ui/comments.cljs:446
msgid "labels.comment.mark-as-solved"
msgstr "Marcar como resuelto"
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:127
msgid "labels.comments"
msgstr "Comentarios"
@ -1957,6 +1972,10 @@ msgstr "Contraseña"
msgid "labels.pending-invitation"
msgstr "Pendiente"
#: src/app/main/ui/comments.cljs:147
msgid "labels.post"
msgstr "Publicar"
#: src/app/main/ui/onboarding/questions.cljs:51
msgid "labels.previous"
msgstr "Anterior"
@ -2003,6 +2022,26 @@ msgstr "Renombrar"
msgid "labels.rename-team"
msgstr "Renombra el equipo"
#: src/app/main/ui/comments.cljs:145
msgid "labels.reply"
msgstr "respuesta"
#: src/app/main/ui/comments.cljs:150
msgid "labels.reply.new"
msgstr "nueva respuesta"
#: src/app/main/ui/comments.cljs:188
msgid "labels.reply.thread"
msgstr "Responder"
#: src/app/main/ui/comments.cljs:146
msgid "labels.replies"
msgstr "respuestas"
#: src/app/main/ui/comments.cljs:151
msgid "labels.replies.new"
msgstr "nuevas respuestas"
#: src/app/main/ui/dashboard/team.cljs:681
msgid "labels.resend-invitation"
msgstr "Reenviar invitacion"