0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -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 = {}) { async showCommentsThread(number, clickOptions = {}) {
await this.page await this.page
.getByTestId("floating-thread-bubble") .getByTestId(`floating-thread-bubble-${number.toString()}`)
.filter({ hasText: number.toString() })
.click(clickOptions); .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 // SELECTS AND DROPDOWNS
.menu-dropdown { .menu-dropdown {
@include menuShadow; @include menuShadow;

View file

@ -502,11 +502,11 @@
(d/update-in-when [:comments-local :draft] merge data))))) (d/update-in-when [:comments-local :draft] merge data)))))
(defn toggle-comment-options (defn toggle-comment-options
[comment] [comment-id]
(ptk/reify ::toggle-comment-options (ptk/reify ::toggle-comment-options
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (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 (defn hide-comment-options
[] []

View file

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

View file

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

View file

@ -9,7 +9,9 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.comments :as dcm] [app.main.data.comments :as dcm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -17,12 +19,15 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-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.main.ui.icons :as i]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.math :refer [floor]]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -93,8 +98,66 @@
:on-change on-change* :on-change on-change*
:max-length max-length}])) :max-length max-length}]))
(mf/defc reply-form (def ^:private schema:comment-avatar
[{:keys [thread] :as props}] [: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) (let [show-buttons? (mf/use-state false)
content (mf/use-state "") content (mf/use-state "")
@ -124,9 +187,10 @@
(fn [] (fn []
(st/emit! (dcm/add-comment thread @content)) (st/emit! (dcm/add-comment thread @content))
(on-cancel)))] (on-cancel)))]
[:div {:class (stl/css :reply-form)} [:div {:class (stl/css :form)}
[:& resizing-textarea {:value @content [:& resizing-textarea {:value @content
:placeholder "Reply" :placeholder (tr "labels.reply.thread")
:autofocus true
:on-blur on-blur :on-blur on-blur
:on-focus on-focus :on-focus on-focus
:select-on-focus? false :select-on-focus? false
@ -134,29 +198,62 @@
:on-change on-change :on-change on-change
:max-length 750}] :max-length 750}]
(when (or @show-buttons? (seq @content)) (when (or @show-buttons? (seq @content))
[:div {:class (stl/css :buttons-wrapper)} [:div {:class (stl/css :form-buttons-wrapper)}
[:input.btn-secondary [:> button* {:variant "ghost"
{:type "button" :on-click on-cancel}
:class (stl/css :cancel-btn) (tr "ds.confirm-cancel")]
:value "Cancel" [:> button* {:variant "primary"
:on-click on-cancel}] :on-click on-submit
[:input :disabled disabled?}
{:type "button" (tr "labels.post")]])]))
:class (stl/css-case :post-btn true
:global/disabled disabled?)
:value "Post"
:on-click on-submit
:disabled disabled?}]])]))
(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]}] [{:keys [draft zoom on-cancel on-submit position-modifier]}]
(let [position (cond-> (:position draft) (let [profile (mf/deref refs/profile)
(some? position-modifier)
(gpt/transform position-modifier))
content (:content draft)
pos-x (* (:x position) zoom) position (cond-> (:position draft)
pos-y (* (:y position) zoom) (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) disabled? (or (str/blank? content)
(str/empty? content)) (str/empty? content))
@ -183,17 +280,18 @@
[:* [:*
[:div [:div
{:class (stl/css :floating-thread-bubble) {:class (stl/css :floating-preview-wrapper)
:data-testid "floating-thread-bubble" :data-testid "floating-thread-bubble"
:style {:top (str pos-y "px") :style {:top (str pos-y "px")
:left (str pos-x "px")} :left (str pos-x "px")}
:on-click dom/stop-propagation} :on-click dom/stop-propagation}
"?"] [:> comment-avatar* {:class (stl/css :avatar-lg)
[:div {:class (stl/css :thread-content) :image (cfg/resolve-profile-photo-url profile)}]]
[:div {:class (stl/css :floating-thread-wrapper)
:style {:top (str (- pos-y 24) "px") :style {:top (str (- pos-y 24) "px")
:left (str (+ pos-x 28) "px")} :left (str (+ pos-x 28) "px")}
:on-click dom/stop-propagation} :on-click dom/stop-propagation}
[:div {:class (stl/css :reply-form)} [:div {:class (stl/css :form)}
[:& resizing-textarea {:placeholder (tr "labels.write-new-comment") [:& resizing-textarea {:placeholder (tr "labels.write-new-comment")
:value (or content "") :value (or content "")
:autofocus true :autofocus true
@ -202,58 +300,89 @@
:on-change on-change :on-change on-change
:on-ctrl-enter on-submit :on-ctrl-enter on-submit
:max-length 750}] :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 (mf/defc comment-floating-thread-header*
:class (stl/css :cancel-btn) {::mf/props :obj
:type "button" ::mf/private true}
:value "Cancel"}] [{: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 toggle-resolved
: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
(mf/use-fn (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/use-fn
(mf/deps @content) (fn [event]
(fn [] (on-submit @content))) (dom/stop-propagation event)
(st/emit! (dcm/toggle-comment-options uuid/zero))))
disabled? (or (str/blank? @content) delete-thread
(str/empty? @content))] (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)} on-delete-thread
[:& resizing-textarea {:value @content (mf/use-fn
:autofocus true (fn [event]
:select-on-focus true (dom/stop-propagation event)
:select-on-focus? false (st/emit! (dcm/hide-comment-options))
:on-ctrl-enter on-submit* (st/emit! (modal/show
:on-change on-change {:type :confirm
:max-length 750}] :title (tr "modals.delete-comment-thread.title")
[:div {:class (stl/css :buttons-wrapper)} :message (tr "modals.delete-comment-thread.message")
[:input {:type "button" :accept-label (tr "modals.delete-comment-thread.accept")
:value "Cancel" :on-accept delete-thread}))))
: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?}]]]))
(mf/defc comment-item on-hide-options
[{:keys [comment thread profiles origin] :as props}] (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)) (let [owner (get profiles (:owner-id comment))
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
options (mf/deref comments-local-options) options (mf/deref comments-local-options)
@ -264,7 +393,7 @@
(mf/deps options) (mf/deps options)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (dcm/toggle-comment-options comment)))) (st/emit! (dcm/toggle-comment-options (:id comment)))))
on-hide-options on-hide-options
(mf/use-fn (mf/use-fn
@ -285,24 +414,6 @@
(mf/deps comment) (mf/deps comment)
#(st/emit! (dcm/delete-comment 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 on-submit
(mf/use-fn (mf/use-fn
(mf/deps comment thread) (mf/deps comment thread)
@ -311,29 +422,15 @@
(st/emit! (dcm/update-comment (assoc comment :content content))))) (st/emit! (dcm/update-comment (assoc comment :content content)))))
on-cancel on-cancel
(mf/use-fn #(reset! edition? false)) (mf/use-fn #(reset! edition? false))]
toggle-resolved [:div {:class (stl/css :floating-thread-item-wrapper)}
(mf/use-fn [:div {:class (stl/css :floating-thread-item)}
(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 :author)} [:div {:class (stl/css :author)}
[:div {:class (stl/css :avatar)} [:> comment-avatar* {:image (cfg/resolve-profile-photo-url owner)}]
[:img {:src (cfg/resolve-profile-photo-url owner)}]] [:div {:class (stl/css :author-identity)}
[:div {:class (stl/css :name)} [:div {:class (stl/css :author-fullname)} (:fullname owner)]
[:div {:class (stl/css :fullname)} (:fullname owner)] [:div {:class (stl/css :author-timeago)} (dt/timeago (:modified-at comment))]]
[: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]])
(when (= (:id profile) (:id owner)) (when (= (:id profile) (:id owner))
[:> icon-button* {:variant "ghost" [:> icon-button* {:variant "ghost"
@ -341,24 +438,21 @@
:on-click on-toggle-options :on-click on-toggle-options
:icon "menu"}])] :icon "menu"}])]
[:div {:class (stl/css :content)} [:div {:class (stl/css :item)}
(if @edition? (if @edition?
[:& edit-form {:content (:content comment) [:> comment-edit-form* {:content (:content comment)
:on-submit on-submit :on-submit on-submit
:on-cancel on-cancel}] :on-cancel on-cancel}]
[:span {:class (stl/css :text)} (:content comment)])]] [:span {:class (stl/css :text)} (:content comment)])]]
[:& dropdown {:show (= options (:id comment)) [:& dropdown {:show (= options (:id comment))
:on-close on-hide-options} :on-close on-hide-options}
[:ul {:class (stl/css :comment-options-dropdown)} [:ul {:class (stl/css :dropdown-menu)}
[:li {:class (stl/css :context-menu-option) [:li {:class (stl/css :dropdown-menu-option)
:on-click on-edit-clicked} :on-click on-edit-clicked}
(tr "labels.edit")] (tr "labels.edit")]
(if thread (when-not thread
[:li {:class (stl/css :context-menu-option) [:li {:class (stl/css :dropdown-menu-option)
:on-click on-delete-thread}
(tr "labels.delete-comment-thread")]
[:li {:class (stl/css :context-menu-option)
:on-click on-delete-comment} :on-click on-delete-comment}
(tr "labels.delete-comment")])]]])) (tr "labels.delete-comment")])]]]))
@ -370,48 +464,54 @@
(let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0}) (let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0})
base-x (+ (* (:x position) zoom) (:offset-x viewport)) base-x (+ (* (:x position) zoom) (:offset-x viewport))
base-y (+ (* (:y position) zoom) (:offset-y viewport)) base-y (+ (* (:y position) zoom) (:offset-y viewport))
x (:x position)
y (:y position)
w (:width viewport) w (:width viewport)
h (:height viewport) h (:height viewport)
comment-width 284 ;; TODO: this is the width set via CSS in an outer container… comment-width 284 ;; TODO: this is the width set via CSS in an outer container…
;; We should probably do this in a different way. ;; We should probably do this in a different way.
orientation-left? (>= (+ base-x comment-width (:x bubble-margin)) w) 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) h-dir (if orientation-left? :left :right)
v-dir (if orientation-top? :top :bottom) v-dir (if orientation-top? :top :bottom)]
x (:x position)
y (:y position)]
{:x x :y y :h-dir h-dir :v-dir v-dir})) {:x x :y y :h-dir h-dir :v-dir v-dir}))
(mf/defc thread-comments (mf/defc comment-floating-thread*
{::mf/wrap [mf/memo]} {::mf/props :obj
::mf/wrap [mf/memo]}
[{:keys [thread zoom profiles origin position-modifier viewport]}] [{:keys [thread zoom profiles origin position-modifier viewport]}]
(let [ref (mf/use-ref) (let [ref (mf/use-ref)
thread-id (:id thread) thread-id (:id thread)
thread-pos (:position thread) thread-pos (:position thread)
base-pos (cond-> thread-pos base-pos (cond-> thread-pos
(some? position-modifier) (some? position-modifier)
(gpt/transform 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. ;; We should probably look for a better way of doing this.
bubble-margin {:x 24 :y 0} bubble-margin {:x 24 :y 24}
pos (offset-position base-pos viewport zoom bubble-margin) pos (offset-position base-pos viewport zoom bubble-margin)
margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1)) margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1))
margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1)) margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1))
pos-x (+ (* (:x pos) zoom) margin-x) pos-x (+ (* (:x pos) zoom) margin-x)
pos-y (- (* (:y pos) zoom) margin-y) pos-y (- (* (:y pos) zoom) margin-y)
comments-ref (mf/with-memo [thread-id] comments-ref (mf/with-memo [thread-id]
(make-comments-ref thread-id)) (make-comments-ref thread-id))
comments-map (mf/deref comments-ref) comments-map (mf/deref comments-ref)
comments (mf/with-memo [comments-map] comments (mf/with-memo [comments-map]
(->> (vals comments-map) (->> (vals comments-map)
(sort-by :created-at))) (sort-by :created-at)))
comment (first comments)] first-comment (first comments)]
(mf/with-effect [thread-id] (mf/with-effect [thread-id]
(st/emit! (dcm/retrieve-comments thread-id))) (st/emit! (dcm/retrieve-comments thread-id)))
@ -423,158 +523,170 @@
(when-let [node (mf/ref-val ref)] (when-let [node (mf/ref-val ref)]
(dom/scroll-into-view-if-needed! node))) (dom/scroll-into-view-if-needed! node)))
(when (some? comment) (when (some? first-comment)
[:div {:class (stl/css-case :thread-content true [:div {:class (stl/css-case :floating-thread-wrapper true
:thread-content-left (= (:h-dir pos) :left) :left (= (:h-dir pos) :left)
:thread-content-top (= (:v-dir pos) :top)) :top (= (:v-dir pos) :top))
:id (str "thread-" thread-id) :id (str "thread-" thread-id)
:style {:left (str pos-x "px") :style {:left (str pos-x "px")
:top (str pos-y "px") :top (str pos-y "px")
:max-height max-height} :max-height max-height}
:on-click dom/stop-propagation} :on-click dom/stop-propagation}
[:div {:class (stl/css :comments)} [:div {:class (stl/css :floating-thread-header)}
[:& comment-item {:comment comment [:> comment-floating-thread-header* {:profiles profiles
:profiles profiles :thread thread
:thread thread :origin origin}]]
:origin origin}]
[:div {:class (stl/css :floating-thread-main)}
[:> comment-floating-thread-item* {:comment first-comment
:profiles profiles
:thread thread}]
(for [item (rest comments)] (for [item (rest comments)]
[:* {:key (dm/str (:id item))} [:* {:key (dm/str (:id item))}
[:& comment-item {:comment item [:> comment-floating-thread-item* {:comment item
:profiles profiles :profiles profiles}]])]
:origin origin}]])]
[:& reply-form {:thread thread}]
[:div {:ref ref}]])))
(defn use-buble [:> comment-reply-form* {:thread thread}]])))
[zoom {:keys [position frame-id]}]
(let [dragging-ref (mf/use-ref false) (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) 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-x nil
:new-position-y nil :new-position-y nil
:new-frame-id frame-id}) :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 on-pointer-down
(mf/use-fn (mf/use-fn
(mf/deps origin was-open? is-open drag?)
(fn [event] (fn [event]
(dom/capture-pointer event) (when (not= origin :viewer)
(mf/set-ref-val! dragging-ref true) (swap! state assoc :grabbing? true)
(mf/set-ref-val! start-ref (dom/get-client-position event)))) (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 on-pointer-up
(mf/use-fn (mf/use-fn
(mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id])) (mf/deps origin thread (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
(fn [event] (fn [event]
(dom/release-pointer event) (when (not= origin :viewer)
(mf/set-ref-val! dragging-ref false) (swap! state assoc :grabbing? false)
(mf/set-ref-val! start-ref nil) (dom/stop-propagation event)
(swap! state assoc :new-position-x nil) (dom/release-pointer event)
(swap! state assoc :new-position-y nil))) (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 on-pointer-move
(mf/use-fn (mf/use-fn
(mf/deps position zoom) (mf/deps origin drag? 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)
(fn [event] (fn [event]
(when (not= origin :viewer) (when (not= origin :viewer)
(mf/set-ref-val! drag? true) (mf/set-ref-val! drag? true)
(dom/stop-propagation event) (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* on-click*
(mf/use-fn (mf/use-fn
(mf/deps origin thread on-click) (mf/deps origin thread on-click was-open? drag? (select-keys @state [:hover?]))
(fn [event] (fn [event]
(dom/stop-propagation 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) (when (= origin :viewer)
(on-click thread))))] (on-click thread))))]
[:div {:style {:top (str pos-y "px") [:div {:style {:top (str pos-y "px")
:left (str pos-x "px")} :left (str pos-x "px")}
:on-pointer-down on-pointer-down* :on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up* :on-pointer-up on-pointer-up
:on-pointer-move on-pointer-move* :on-pointer-move on-pointer-move
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave
:on-click on-click* :on-click on-click*
:on-lost-pointer-capture on-lost-pointer-capture :class (stl/css-case :floating-preview-wrapper true
:data-testid "floating-thread-bubble" :floating-preview-bubble (false? (:hover? @state))
:class (stl/css-case :grabbing (true? (:grabbing? @state)))}
:floating-thread-bubble true
:resolved (:is-resolved thread)
:unread (pos? (:count-unread-comments thread)))}
[:span (:seqn thread)]]))
(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]}] [{:keys [item profiles on-click]}]
(let [owner (get profiles (:owner-id item)) (let [owner (get profiles (:owner-id item))
frame (mf/deref (refs/workspace-page-object-by-id (:page-id item) (:frame-id item)))
on-click* on-click*
(mf/use-fn (mf/use-fn
(mf/deps item) (mf/deps item)
@ -584,52 +696,64 @@
(when (fn? on-click) (when (fn? on-click)
(on-click item))))] (on-click item))))]
[:div {:class (stl/css :comment) [:div {:class (stl/css :cover)
:on-click on-click*} :on-click on-click*}
[:div {:class (stl/css :author)} [:div {:class (stl/css :location)}
[:div {:class (stl/css-case :thread-bubble true [:div {:class (stl/css :location-text)}
:resolved (:is-resolved item) (str "#" (:seqn item))
:unread (pos? (:count-unread-comments item)))} (str " - " (:page-name item))
(:seqn item)] (when (and (some? frame) (not (cfh/root? frame)))
[:div {:class (stl/css :avatar)} (str " - " (:name frame)))]]
[: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")]))
(when (and (> total 1) (> unread 0)) [:> comment-info* {:item item
(if (= unread 1) :profile owner}]]))
[:span {:class (stl/css :new-replies)} "1 new reply"]
[:span {:class (stl/css :new-replies)} (str unread " new replies")]))])]]))
(mf/defc comment-thread-group (mf/defc comment-sidebar-thread-group*
{::mf/props :obj}
[{:keys [group profiles on-thread-click]}] [{:keys [group profiles on-thread-click]}]
[:div {:class (stl/css :thread-group)} [:div
(if (:file-name group) (for [item (:items group)]
[:div {:class (stl/css :section-title) [:> comment-sidebar-thread-item*
:title (str (:file-name group) ", " (:page-name group))} {:item item
[:span {:class (stl/css :file-name)} (:file-name group) ", "] :on-click on-thread-click
[:span {:class (stl/css :page-name)} (:page-name group)]] :profiles profiles
:key (:id item)}])])
[:div {:class (stl/css :section-title) (mf/defc comment-dashboard-thread-item*
:title (:page-name group)} {::mf/props :obj
[:span {:class (stl/css :icon)} i/document] ::mf/private true}
[:span {:class (stl/css :page-name)} (:page-name group)]]) [{:keys [item profiles on-click]}]
(let [owner (get profiles (:owner-id item))
[:div {:class (stl/css :threads)} on-click*
(for [item (:items group)] (mf/use-fn
[:& comment-thread (mf/deps item)
{:item item (fn [event]
:on-click on-thread-click (dom/stop-propagation event)
:profiles profiles (dom/prevent-default event)
:key (:id item)}])]]) (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"; @import "refactor/common-refactor.scss";
// Comment-thread-group .grayed-text {
.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;
color: var(--comment-subtitle-color); color: var(--comment-subtitle-color);
} }
.page-name { .location {
@include textEllipsis;
color: var(--comment-subtitle-color); color: var(--comment-subtitle-color);
}
.icon {
display: flex; display: flex;
align-items: center; 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; gap: $s-8;
} }
.thread-bubble { .location-icon {
@extend .comment-bubbles; display: flex;
&.resolved { }
@extend .resolved-comment-bubble;
} .location-text {
&.unread { @include textEllipsis;
@extend .unread-comment-bubble; }
}
.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 { .avatar {
position: relative;
height: $s-24;
width: $s-24;
border-radius: $br-circle;
}
.avatar-lg {
height: $s-32; height: $s-32;
width: $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; border-radius: $br-circle;
img {
border-radius: $br-circle;
}
} }
.name { .avatar-mask {
flex-grow: 1; border-radius: $br-circle;
.fullname { position: absolute;
@include textEllipsis; height: 100%;
color: var(--comment-title-color); width: 100%;
} left: 0;
.timeago { top: 0;
@include textEllipsis;
color: var(--comment-subtitle-color);
}
} }
.content { .avatar-darken {
position: relative; 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; @include bodySmallTypography;
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
word-wrap: break-word; word-wrap: break-word;
@ -112,119 +107,128 @@
} }
.replies { .replies {
@include bodySmallTypography;
display: flex; display: flex;
gap: $s-8; gap: $s-8;
} }
.total-replies { .replies-total {
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
} }
.new-replies { .replies-unread {
color: var(--color-accent-primary); color: var(--color-accent-primary);
} }
// Thread-bubble
.floating-thread-bubble { .floating-preview-wrapper {
@extend .comment-bubbles; z-index: $z-index-1;
position: absolute; position: absolute;
user-select: none;
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
transform: translate(calc(-1 * $s-16), calc(-1 * $s-16)); transform: translate(calc(-1 * $s-16), calc(-1 * $s-16));
&.resolved {
@extend .resolved-comment-bubble;
}
&.unread {
@extend .unread-comment-bubble;
}
} }
// thread-content .floating-preview-bubble {
.thread-content { z-index: initial;
position: absolute; }
overflow-y: auto;
width: $s-284;
padding: $s-12;
padding-inline-end: $s-8;
.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; pointer-events: auto;
user-select: text;
border-radius: $br-8; border-radius: $br-8;
border: $s-2 solid var(--modal-border-color); border: $s-2 solid var(--modal-border-color);
background-color: var(--comment-modal-background-color); background-color: var(--comment-modal-background-color);
--translate-x: 0%; --translate-x: 0%;
--translate-y: 0%; --translate-y: 0%;
transform: translate(var(--translate-x), var(--translate-y)); transform: translate(var(--translate-x), var(--translate-y));
.comments { &.left {
display: flex; --translate-x: -100%;
flex-direction: column; }
gap: $s-24; &.top {
--translate-y: -100%;
} }
} }
.thread-content-left { .floating-thread-header {
--translate-x: -100%;
}
.thread-content-top {
--translate-y: -100%;
}
// comment-item
.comment-container {
position: relative; position: relative;
.comment { display: flex;
@include bodySmallTypography; justify-content: space-between;
.author { align-items: center;
display: flex; height: $s-32;
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;
}
}
} }
// edit-form & reply-form .floating-thread-header-left {
@include bodySmallTypography;
color: var(--color-foreground-primary);
}
.edit-form, .floating-thread-header-right {
.reply-form { 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 { textarea {
@extend .input-element; @extend .input-element;
@include bodySmallTypography; @include bodySmallTypography;
@ -232,8 +236,8 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
max-width: $s-260; max-width: $s-260;
margin-bottom: $s-8;
padding: $s-8; padding: $s-8;
margin-top: $s-4;
color: var(--input-foreground-color-active); color: var(--input-foreground-color-active);
resize: vertical; resize: vertical;
&:focus { &:focus {
@ -241,21 +245,10 @@
outline: none; outline: none;
} }
} }
.buttons-wrapper { }
display: flex;
justify-content: flex-end; .form-buttons-wrapper {
gap: $s-4; display: flex;
.post-btn { justify-content: flex-end;
@extend .button-primary; gap: $s-8;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
.cancel-btn {
@extend .button-secondary;
height: $s-32;
width: $s-92;
margin-bottom: 0;
}
}
} }

View file

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

View file

@ -44,21 +44,16 @@
} }
.comment-button { .comment-button {
@include buttonStyle; position: relative;
@include flexCenter; .unread {
border-radius: $br-8; position: absolute;
height: $s-32; width: $s-8;
width: $s-32; height: $s-8;
--comment-icon-small-foreground-color: var(--icon-foreground); border: $s-2 solid var(--color-background-tertiary);
border-radius: 50%;
&.unread, background: red;
&.open { top: $s-6;
--comment-icon-small-foreground-color: var(--icon-foreground-selected); right: $s-6;
}
&:hover {
background-color: var(--color-background-quaternary);
--comment-icon-small-foreground-color: var(--icon-foreground-active);
} }
} }
@ -100,13 +95,3 @@
flex-grow: 1; flex-grow: 1;
text-transform: uppercase; 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.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.components.link :refer [link]] [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.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]]
[app.main.ui.dashboard.team-form] [app.main.ui.dashboard.team-form]
@ -1071,9 +1071,8 @@
(tr "labels.logout")]] (tr "labels.logout")]]
(when (and team profile) (when (and team profile)
[:& comments-icon [:> comments-icon*
{:profile profile {:profile profile
:show? show-comments?
:on-show-comments handle-show-comments}])]])) :on-show-comments handle-show-comments}])]]))
(mf/defc sidebar* (mf/defc sidebar*

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -748,6 +748,14 @@ msgstr "No matches found for “%s“"
msgid "dashboard.no-projects-placeholder" msgid "dashboard.no-projects-placeholder"
msgstr "Pinned projects will appear here" 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 #: src/app/main/ui/auth/verify_token.cljs:32
msgid "dashboard.notifications.email-changed-successfully" msgid "dashboard.notifications.email-changed-successfully"
msgstr "Your email address has been updated successfully" msgstr "Your email address has been updated successfully"
@ -1629,6 +1637,13 @@ msgstr "Close"
msgid "labels.collapse" msgid "labels.collapse"
msgstr "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 #: 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" msgid "labels.comments"
msgstr "Comments" msgstr "Comments"
@ -1952,6 +1967,10 @@ msgstr "Password"
msgid "labels.pending-invitation" msgid "labels.pending-invitation"
msgstr "Pending" msgstr "Pending"
#: src/app/main/ui/comments.cljs:147
msgid "labels.post"
msgstr "Post"
#: src/app/main/ui/onboarding/questions.cljs:51 #: src/app/main/ui/onboarding/questions.cljs:51
msgid "labels.previous" msgid "labels.previous"
msgstr "Previous" msgstr "Previous"
@ -1998,6 +2017,26 @@ msgstr "Rename"
msgid "labels.rename-team" msgid "labels.rename-team"
msgstr "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 #: src/app/main/ui/dashboard/team.cljs:681
msgid "labels.resend-invitation" msgid "labels.resend-invitation"
msgstr "Resend invitation" msgstr "Resend invitation"

View file

@ -759,6 +759,14 @@ msgstr "No se encuentra “%s“"
msgid "dashboard.no-projects-placeholder" msgid "dashboard.no-projects-placeholder"
msgstr "Los proyectos fijados aparecerán aquí" 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 #: src/app/main/ui/auth/verify_token.cljs:32
msgid "dashboard.notifications.email-changed-successfully" msgid "dashboard.notifications.email-changed-successfully"
msgstr "Tu dirección de correo ha sido actualizada" msgstr "Tu dirección de correo ha sido actualizada"
@ -1634,6 +1642,13 @@ msgstr "Cerrar"
msgid "labels.collapse" msgid "labels.collapse"
msgstr "Colapsar" 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 #: 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" msgid "labels.comments"
msgstr "Comentarios" msgstr "Comentarios"
@ -1957,6 +1972,10 @@ msgstr "Contraseña"
msgid "labels.pending-invitation" msgid "labels.pending-invitation"
msgstr "Pendiente" msgstr "Pendiente"
#: src/app/main/ui/comments.cljs:147
msgid "labels.post"
msgstr "Publicar"
#: src/app/main/ui/onboarding/questions.cljs:51 #: src/app/main/ui/onboarding/questions.cljs:51
msgid "labels.previous" msgid "labels.previous"
msgstr "Anterior" msgstr "Anterior"
@ -2003,6 +2022,26 @@ msgstr "Renombrar"
msgid "labels.rename-team" msgid "labels.rename-team"
msgstr "Renombra el equipo" 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 #: src/app/main/ui/dashboard/team.cljs:681
msgid "labels.resend-invitation" msgid "labels.resend-invitation"
msgstr "Reenviar invitacion" msgstr "Reenviar invitacion"