diff --git a/docs/user-guide/account-teams/comments.njk b/docs/user-guide/account-teams/comments.njk
index bec75f4858..4d5a2a769b 100644
--- a/docs/user-guide/account-teams/comments.njk
+++ b/docs/user-guide/account-teams/comments.njk
@@ -8,7 +8,7 @@ desc: Learn how to import and export files in Penpot, the free, open-source desi
Comments allow the team to have one priceless conversation getting and providing feedback right over the designs and prototypes.
-At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the C key. More about comments at the Workspace
+At the workspace, activate the comment tool by clicking the comment icon in the navbar or pressing the C key. Comment bubbles remain visible while you design, so you can read and manage feedback without leaving your design tools. Use View → Hide comments / Show comments or Ctrl+Shift+C (Cmd+Shift+C on macOS) to toggle bubbles on the canvas. More about comments at the Workspace
URLs in comments are detected automatically and open in a new browser tab when clicked.
diff --git a/docs/user-guide/designing/workspace-basics.njk b/docs/user-guide/designing/workspace-basics.njk
index f6cc0f14dd..e87ff0d740 100644
--- a/docs/user-guide/designing/workspace-basics.njk
+++ b/docs/user-guide/designing/workspace-basics.njk
@@ -160,6 +160,9 @@ press Shift/⇧ + left click over the right arrow of a group or a boa
Write your comment in the text box.
Press Post to leave the comment or Cancel to not do it.
+Comment bubbles stay visible on the canvas while you design. You can reply to, edit, and manage threads without staying in Comments mode.
+Use View → Hide comments or View → Show comments (after Lock guides) to toggle comment bubbles on the canvas. You can also press Ctrl+Shift+C (Cmd+Shift+C on macOS). This preference is saved in your user settings.
+Press C or the comment button to enter Comments mode when you want to add a new comment on the canvas. Comments mode still works as before and shows comments if they were hidden.
URLs in comments are detected automatically and open in a new browser tab when clicked.
How to reply a comment
diff --git a/docs/user-guide/first-steps/shortcuts.njk b/docs/user-guide/first-steps/shortcuts.njk
index f653090fd6..76b323ec31 100644
--- a/docs/user-guide/first-steps/shortcuts.njk
+++ b/docs/user-guide/first-steps/shortcuts.njk
@@ -211,6 +211,11 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
| CtrlShiftR |
⌘⇧R |
+
+ | Show/hide comments |
+ CtrlShiftC |
+ ⌘⇧C |
+
| Show/hide shortcuts |
? |
diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index a617753fa3..450268cbd1 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -16,14 +16,17 @@
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
+ [app.main.data.notifications :as ntf]
[app.main.data.workspace.common :as dwco]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
+ [app.main.data.workspace.layout :as dwlo]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.zoom :as dwz]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.streams :as ms]
+ [app.util.i18n :refer [tr]]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -31,6 +34,18 @@
(declare handle-interrupt)
(declare handle-comment-layer-click)
+(defn toggle-comments-visibility
+ [& {:keys [origin] :as _opts}]
+ (ptk/reify ::toggle-comments-visibility
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [visible? (contains? (:workspace-layout state) :display-comments)]
+ (rx/of (vary-meta (dwlo/toggle-layout-flag :display-comments)
+ assoc ::ev/origin (or origin "workspace"))
+ (ntf/success (tr (if visible?
+ "workspace.toast.comments-hidden"
+ "workspace.toast.comments-visible"))))))))
+
(defn initialize-comments
[file-id]
(ptk/reify ::initialize-comments
@@ -58,12 +73,18 @@
(ptk/reify ::handle-interrupt
ptk/WatchEvent
(watch [_ state _]
- (let [local (:comments-local state)]
+ (let [local (:comments-local state)
+ comments-mode? (= :comments (get-in state [:workspace-drawing :tool]))]
(cond
(:draft local) (rx/of (dcmt/close-thread))
(:open local) (rx/of (dcmt/close-thread))
- :else (rx/of (dwe/clear-edition-mode)
- (dws/deselect-all true)))))))
+ ;; Only clear edition / deselect on interrupt while the comments
+ ;; tool is active. When comments are merely visible during design,
+ ;; `select-shape` emits `:interrupt` and this would otherwise wipe
+ ;; the freshly selected shape, breaking click selection.
+ comments-mode? (rx/of (dwe/clear-edition-mode)
+ (dws/deselect-all true))
+ :else (rx/empty))))))
;; Event responsible of the what should be executed when user clicked
;; on the comments layer. An option can be create a new draft thread,
@@ -74,15 +95,17 @@
(ptk/reify ::handle-comment-layer-click
ptk/WatchEvent
(watch [_ state _]
- (let [local (:comments-local state)]
- (if (some? (:open local))
- (rx/of (dcmt/close-thread))
- (let [page-id (:current-page-id state)
- file-id (:current-file-id state)
- params {:position position
- :page-id page-id
- :file-id file-id}]
- (rx/of (dcmt/create-draft params))))))))
+ (if (not= :comments (get-in state [:workspace-drawing :tool]))
+ (rx/empty)
+ (let [local (:comments-local state)]
+ (if (some? (:open local))
+ (rx/of (dcmt/close-thread))
+ (let [page-id (:current-page-id state)
+ file-id (:current-file-id state)
+ params {:position position
+ :page-id page-id
+ :file-id file-id}]
+ (rx/of (dcmt/create-draft params)))))))))
(defn center-to-comment-thread
[{:keys [position] :as thread}]
@@ -126,7 +149,18 @@
thread-id (:id thread)]
(rx/concat
- (rx/of #(update % :comment-threads assoc thread-id thread))
+ (rx/of (fn [state]
+ (-> state
+ (update :comment-threads assoc thread-id thread)
+ ;; Keep the page positions map in sync so subsequent
+ ;; frame moves compute the relative offset from the
+ ;; latest position instead of a stale one.
+ (dsh/update-page page-id
+ #(update-in % [:comment-thread-positions thread-id]
+ (fn [pos]
+ (-> pos
+ (assoc :position (:position thread))
+ (assoc :frame-id (:frame-id thread)))))))))
(->> (rp/cmd! :update-comment-thread-position thread)
(rx/catch #(rx/throw {:type :update-comment-thread-position}))
(rx/ignore))))))))
diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs
index 68cd8c6b92..03d7545bb8 100644
--- a/frontend/src/app/main/data/workspace/drawing.cljs
+++ b/frontend/src/app/main/data/workspace/drawing.cljs
@@ -14,6 +14,7 @@
[app.main.data.workspace.drawing.box :as box]
[app.main.data.workspace.drawing.common :as common]
[app.main.data.workspace.drawing.curve :as curve]
+ [app.main.data.workspace.layout :as dwlo]
[app.main.data.workspace.path :as path]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -57,6 +58,9 @@
;; NOTE: comments are a special case and they manage they
;; own interrupt cycle.
+ (when (= tool :comments)
+ (rx/of (dwlo/toggle-layout-flag :display-comments :force? true)))
+
(when (and (not= tool :comments)
(not= tool :path))
(let [stopper (rx/filter (ptk/type? ::clear-drawing) stream)]
diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs
index 2c006b4988..2f04bcfa53 100644
--- a/frontend/src/app/main/data/workspace/layout.cljs
+++ b/frontend/src/app/main/data/workspace/layout.cljs
@@ -25,6 +25,7 @@
:element-options
:rulers
:display-guides
+ :display-comments
:lock-guides
:snap-guides
:scale-text
@@ -60,6 +61,7 @@
:element-options
:rulers
:display-guides
+ :display-comments
:snap-guides
:dynamic-alignment
:display-artboard-names
@@ -143,7 +145,8 @@
{:hide-palettes :app.main.data.workspace/hide-palettes?
:colorpalette :app.main.data.workspace/show-colorpalette?
:textpalette :app.main.data.workspace/show-textpalette?
- :rulers :app.main.data.workspace/show-rulers?})
+ :rulers :app.main.data.workspace/show-rulers?
+ :display-comments :app.main.data.workspace/show-comments?})
(defn load-layout-flags
"Given the current layout flags, and updates them with the data
diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs
index a68934efc0..5450a6d7a7 100644
--- a/frontend/src/app/main/data/workspace/shortcuts.cljs
+++ b/frontend/src/app/main/data/workspace/shortcuts.cljs
@@ -16,6 +16,7 @@
[app.main.data.shortcuts :as ds]
[app.main.data.workspace :as dw]
[app.main.data.workspace.colors :as mdc]
+ [app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.libraries :as dwl]
@@ -322,6 +323,12 @@
:subsections [:tools]
:fn #(st/emit! (dwd/select-for-drawing :comments))}
+ :toggle-comments-visibility
+ {:tooltip (ds/meta-shift "C")
+ :command (ds/c-mod "shift+c")
+ :subsections [:main-menu]
+ :fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))}
+
:insert-image {:tooltip (ds/shift "K")
:command "shift+k"
:subsections [:tools]
diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs
index 8b87b01d5a..84ac8883e1 100644
--- a/frontend/src/app/main/ui/workspace/main_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/main_menu.cljs
@@ -22,6 +22,7 @@
[app.main.data.profile :as du]
[app.main.data.shortcuts :as scd]
[app.main.data.workspace :as dw]
+ [app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.mcp :as mcp]
[app.main.data.workspace.shortcuts :as sc]
@@ -355,7 +356,15 @@
(r/set-resize-type! :bottom)
(st/emit! (dw/remove-layout-flag :colorpalette)
(-> (dw/toggle-layout-flag :textpalette)
- (vary-meta assoc ::ev/origin "workspace:menu")))))]
+ (vary-meta assoc ::ev/origin "workspace:menu")))))
+
+ toggle-comments-visibility
+ (mf/use-fn
+ (mf/deps on-close)
+ (fn [event]
+ (dom/stop-propagation event)
+ (st/emit! (dwcm/toggle-comments-visibility {:origin "workspace:menu"}))
+ (on-close)))]
[:> dropdown-menu* {:show true
:class (stl/css :base-menu :sub-menu :pos-3)
@@ -400,6 +409,19 @@
(tr "workspace.header.menu.unlock-guides")
(tr "workspace.header.menu.lock-guides"))]]
+ [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
+ :on-click toggle-comments-visibility
+ :on-key-down (fn [event]
+ (when (kbd/enter? event)
+ (toggle-comments-visibility event)))
+ :data-testid "display-comments"
+ :id "file-menu-comments"}
+ [:span {:class (stl/css :item-name)}
+ (if (contains? layout :display-comments)
+ (tr "workspace.header.menu.hide-comments")
+ (tr "workspace.header.menu.show-comments"))]
+ [:> shortcuts* {:id :toggle-comments-visibility}]]
+
(when-not ^boolean read-only?
[:*
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 3c5fb33114..ce03ca1f3a 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -223,7 +223,9 @@
on-frame-select (actions/on-frame-select selected read-only?)
disable-events? (contains? layout :comments)
- show-comments? (= drawing-tool :comments)
+ comments-mode? (= drawing-tool :comments)
+ show-comments? (or comments-mode?
+ (contains? layout :display-comments))
show-cursor-tooltip? tooltip
show-draw-area? drawing-obj
show-gradient-handlers? (= (count selected) 1)
diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs
index f4b3e9d422..8468b29b4b 100644
--- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs
@@ -8,6 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
+ [app.common.geom.matrix :as gmt]
+ [app.common.geom.point :as gpt]
+ [app.common.geom.shapes :as gsh]
[app.main.data.comments :as dcm]
[app.main.data.workspace.comments :as dwcm]
[app.main.refs :as refs]
@@ -31,12 +34,38 @@
threads-map (mf/deref refs/threads)
+ ;; Active transform modifiers (e.g. while dragging a board). We use
+ ;; them to move comment bubbles live alongside their frame, instead of
+ ;; only repositioning them at drop time. The SVG (legacy) renderer keeps
+ ;; them in `:workspace-modifiers`, while the WASM renderer pushes them
+ ;; through the `wasm-modifiers` stream as plain transform matrices.
+ modifiers (mf/deref refs/workspace-modifiers)
+ wasm-mods (into {} (mf/deref refs/workspace-wasm-modifiers))
+ objects (mf/deref refs/workspace-page-objects)
+
threads
(mf/with-memo [threads-map local profile page-id]
(->> (vals threads-map)
(filter #(= (:page-id %) page-id))
(dcm/apply-filters local profile)))
+ ;; Returns the position translation matrix for a frame that is being
+ ;; transformed, or nil when the frame has no active modifier. The delta
+ ;; matches `move-frame-comment-threads` (frame top-left displacement) so
+ ;; the bubble does not jump when the modifier is committed.
+ frame-position-modifier
+ (fn [frame-id]
+ (when-let [frame (get objects frame-id)]
+ (let [frame'
+ (if-let [modifier (get-in modifiers [frame-id :modifiers])]
+ (gsh/transform-shape frame modifier)
+ (when-let [transform (get wasm-mods frame-id)]
+ (gsh/apply-transform frame transform)))]
+ (when (some? frame')
+ (let [delta (gpt/to-vec (gpt/point (:x frame) (:y frame))
+ (gpt/point (:x frame') (:y frame')))]
+ (gmt/translate-matrix delta))))))
+
viewport
(assoc vport :offset-x pos-x :offset-y pos-y)
@@ -67,9 +96,11 @@
(if group?
[:> cmt/comment-floating-group* {:thread-group thread-group
:zoom zoom
+ :position-modifier (frame-position-modifier (:frame-id thread))
:key (:seqn thread)}]
[:> cmt/comment-floating-bubble* {:thread thread
:zoom zoom
+ :position-modifier (frame-position-modifier (:frame-id thread))
:is-open (= (:id thread) (:open local))
:key (:seqn thread)}])))
@@ -79,6 +110,7 @@
[:> cmt/comment-floating-thread*
{:thread thread
:viewport viewport
+ :position-modifier (frame-position-modifier (:frame-id thread))
:zoom zoom}])))
(when-let [draft (:draft local)]
diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
index 45d2b5177e..ed2deb2646 100644
--- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs
@@ -333,7 +333,9 @@
on-frame-select (actions/on-frame-select selected read-only?)
disable-events? (contains? layout :comments)
- show-comments? (= drawing-tool :comments)
+ comments-mode? (= drawing-tool :comments)
+ show-comments? (or comments-mode?
+ (contains? layout :display-comments))
show-cursor-tooltip? tooltip
show-draw-area? drawing-obj
show-gradient-handlers? (= (count selected) 1)
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 1faf2b134d..60e3a8abb4 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -6767,6 +6767,18 @@ msgstr "Hide fonts palette"
msgid "workspace.header.menu.lock-guides"
msgstr "Lock guides"
+msgid "workspace.header.menu.show-comments"
+msgstr "Show comments"
+
+msgid "workspace.header.menu.hide-comments"
+msgstr "Hide comments"
+
+msgid "workspace.toast.comments-visible"
+msgstr "Comments visible"
+
+msgid "workspace.toast.comments-hidden"
+msgstr "Comments hidden"
+
#: src/app/main/ui/workspace/main_menu.cljs:853
msgid "workspace.header.menu.mcp.plugin.status.connect"
msgstr "Connect"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index 97774a7169..97a32ab802 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -6596,6 +6596,18 @@ msgstr "Ocultar nombres de tableros"
msgid "workspace.header.menu.hide-guides"
msgstr "Ocultar guías"
+msgid "workspace.header.menu.show-comments"
+msgstr "Mostrar comentarios"
+
+msgid "workspace.header.menu.hide-comments"
+msgstr "Ocultar comentarios"
+
+msgid "workspace.toast.comments-visible"
+msgstr "Comentarios visibles"
+
+msgid "workspace.toast.comments-hidden"
+msgstr "Comentarios ocultos"
+
#: src/app/main/ui/workspace/main_menu.cljs:413
msgid "workspace.header.menu.hide-palette"
msgstr "Ocultar paleta de colores"