From b5e5c4b0dd603745749bce82730de1670c832970 Mon Sep 17 00:00:00 2001 From: luisddm Date: Mon, 9 Dec 2024 17:16:27 +0100 Subject: [PATCH] :recycle: Visual changes in comments --- frontend/playwright/ui/pages/ViewerPage.js | 3 +- .../styles/common/refactor/basic-rules.scss | 22 - frontend/src/app/main/data/comments.cljs | 4 +- .../src/app/main/data/workspace/comments.cljs | 12 +- frontend/src/app/main/refs.cljs | 6 +- frontend/src/app/main/ui/comments.cljs | 728 ++++++++++-------- frontend/src/app/main/ui/comments.scss | 351 +++++---- .../src/app/main/ui/dashboard/comments.cljs | 64 +- .../src/app/main/ui/dashboard/comments.scss | 35 +- .../src/app/main/ui/dashboard/sidebar.cljs | 5 +- frontend/src/app/main/ui/viewer/comments.cljs | 11 +- .../src/app/main/ui/workspace/comments.cljs | 4 +- .../src/app/main/ui/workspace/comments.scss | 1 - .../src/app/main/ui/workspace/viewport.cljs | 2 +- .../main/ui/workspace/viewport/comments.cljs | 25 +- frontend/translations/en.po | 39 + frontend/translations/es.po | 39 + 17 files changed, 745 insertions(+), 606 deletions(-) diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js index 311c0c45f..7a2a96bce 100644 --- a/frontend/playwright/ui/pages/ViewerPage.js +++ b/frontend/playwright/ui/pages/ViewerPage.js @@ -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); } diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 5096ecd6a..8b43750ab 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -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; diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 3dc86b5a3..1f631203d 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -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 [] diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index d1b250cc1..1688ec332 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -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 diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 88f8383f8..04a099208 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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? diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 643c35d23..4f64a246d 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -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)}])]) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index d90a158dd..229c13932 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -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; } diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index c2b77134a..b0700e735 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss index af55f6dd1..93070c2f7 100644 --- a/frontend/src/app/main/ui/dashboard/comments.scss +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -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); -} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index dfb3691d7..a4f327015 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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* diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index 2ab261bb3..45c023a1b 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index 7fa35a9af..22da3f663 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/comments.scss b/frontend/src/app/main/ui/workspace/comments.scss index 0d9026a78..af3fa9d43 100644 --- a/frontend/src/app/main/ui/workspace/comments.scss +++ b/frontend/src/app/main/ui/workspace/comments.scss @@ -125,7 +125,6 @@ .thread-groups { display: flex; flex-direction: column; - gap: $s-24; } .thread-group-placeholder { diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 8aa9942b8..33f0cc925 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs index 1b226dcae..c7e92dffd 100644 --- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs @@ -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}])]]])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3e2066af4..972ec32b6 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 00e1a30f0..8b481c78b 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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"