Replace overlapping bubbles with a bubble group (#6059)

This commit is contained in:
luisδμ 2025-03-12 14:37:39 +01:00 committed by GitHub
parent 0efbebd94f
commit 86022a967c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 299 additions and 46 deletions

View file

@ -20,6 +20,7 @@
[app.main.data.workspace.drawing :as dwd]
[app.main.data.workspace.edition :as dwe]
[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]
@ -102,26 +103,6 @@
ny (- (:y position) nh)]
(update local :vbox assoc :x nx :y ny)))))))
(defn navigate
[thread]
(dm/assert!
"expected valid comment thread"
(dcmt/check-comment-thread! thread))
(ptk/reify ::open-comment-thread
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (dcm/go-to-workspace :file-id (:file-id thread)
:page-id (:page-id thread)))
(->> stream
(rx/filter (ptk/type? ::dcmt/comment-threads-fetched))
(rx/take 1)
(rx/mapcat #(rx/of (center-to-comment-thread thread)
(dwd/select-for-drawing :comments)
(with-meta (dcmt/open-thread thread)
{::ev/origin "workspace"}))))))))
(defn update-comment-thread-position
([thread [new-x new-y]]
(update-comment-thread-position thread [new-x new-y] nil))
@ -192,6 +173,66 @@
(map build-move-event)
(rx/from))))))
(defn overlap-bubbles?
"Detect if two bubbles overlap"
[zoom thread-1 thread-2]
(let [distance (gpt/distance (:position thread-1) (:position thread-2))
distance-zoom (* distance zoom)
distance-overlap 32]
(< distance-zoom distance-overlap)))
(defn- calculate-zoom-scale-to-ungroup-current-bubble
"Calculate the minimum zoom scale needed to keep the current bubble ungrouped from the rest"
[zoom thread threads]
(let [threads-rest (filterv #(not= (:id %) (:id thread)) threads)
zoom-scale-step 1.75]
(if (some #(overlap-bubbles? zoom thread %) threads-rest)
(calculate-zoom-scale-to-ungroup-current-bubble (* zoom zoom-scale-step) thread threads)
zoom)))
(defn set-zoom-to-separate-grouped-bubbles
[thread]
(dm/assert!
"zoom-to-separate-bubbles"
(dcmt/check-comment-thread! thread))
(ptk/reify ::set-zoom-to-separate-grouped-bubbles
ptk/WatchEvent
(watch [_ state _]
(let [local (:workspace-local state)
zoom (:zoom local)
page-id (:page-id thread)
threads-map (:comment-threads state)
threads-all (vals threads-map)
threads (filterv #(= (:page-id %) page-id) threads-all)
updated-zoom (calculate-zoom-scale-to-ungroup-current-bubble zoom thread threads)
scale-zoom (/ updated-zoom zoom)]
(rx/of (dwz/set-zoom scale-zoom))))))
(defn navigate-to-comment-from-dashboard
[thread]
(dm/assert!
"expected valid comment thread"
(dcmt/check-comment-thread! thread))
(ptk/reify ::navigate-to-comment-from-dashboard
ptk/WatchEvent
(watch [_ _ stream]
(rx/merge
(rx/of (dcm/go-to-workspace :file-id (:file-id thread)
:page-id (:page-id thread)))
(->> stream
(rx/filter (ptk/type? :app.main.data.workspace/workspace-initialized))
(rx/observe-on :async)
(rx/take 1)
(rx/mapcat #(rx/of (dwd/select-for-drawing :comments)
(set-zoom-to-separate-grouped-bubbles thread)
(center-to-comment-thread thread)
(with-meta (dcmt/open-thread thread)
{::ev/origin "workspace"}))))))))
(defn navigate-to-comment
[thread]
(ptk/reify ::navigate-to-comment
@ -208,6 +249,7 @@
(rx/empty))
(->> (rx/of
(dwd/select-for-drawing :comments)
(set-zoom-to-separate-grouped-bubbles thread)
(center-to-comment-thread thread)
(with-meta (dcmt/open-thread thread) {::ev/origin "workspace"}))
(rx/observe-on :async))))))

View file

@ -17,6 +17,7 @@
[app.main.data.comments :as dcm]
[app.main.data.modal :as modal]
[app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.zoom :as dwz]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
@ -589,22 +590,25 @@
(def ^:private schema:comment-avatar
[:map
[:class {:optional true} :string]
[:image :string]
[:image {:optional true} :string]
[:variant {:optional true}
[:maybe [:enum "read" "unread" "solved"]]]])
(mf/defc comment-avatar*
{::mf/schema schema:comment-avatar}
[{:keys [image variant class] :rest props}]
[{:keys [image variant class children] :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})]
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)}]
(if image
[:img {:src image
:class (stl/css :avatar-image)}]
[:div {:class (stl/css :avatar-text)} children])
[:div {:class (stl/css-case :avatar-mask true
:avatar-darken (= variant "solved"))}]]))
@ -1072,22 +1076,83 @@
[:> mentions-panel*]])]))
(defn group-bubbles
"Group bubbles in different vectors by proximity"
([zoom circles]
(group-bubbles zoom circles [] []))
([zoom circles visited groups]
(if (empty? circles)
groups
(let [current (first circles)
remaining (rest circles)
overlapping-group (some (fn [group]
(when (some (partial dwcm/overlap-bubbles? zoom current) group) group))
groups)]
(if overlapping-group
(group-bubbles zoom remaining visited (map (fn [group]
(if (= group overlapping-group)
(cons current group)
group))
groups))
(group-bubbles zoom remaining visited (cons [current] groups)))))))
(defn- calculate-zoom-scale-to-ungroup-bubbles
"Calculate the minimum zoom scale needed for a group of bubbles to avoid overlap among them"
[zoom threads]
(let [num-threads (count threads)
num-grouped-threads (count (group-bubbles zoom threads))
zoom-scale-step 1.75]
(if (= num-threads num-grouped-threads)
zoom
(calculate-zoom-scale-to-ungroup-bubbles (* zoom zoom-scale-step) threads))))
(mf/defc comment-floating-group*
{::mf/wrap [mf/memo]}
[{:keys [thread-group zoom position-modifier]}]
(let [positions (mapv :position thread-group)
position (gpt/center-points positions)
position (cond-> position
(some? position-modifier)
(gpt/transform position-modifier))
pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom)
unread? (some #(pos? (:count-unread-comments %)) thread-group)
num-threads (str (count thread-group))
test-id (str/join "-" (map :seqn (sort-by :seqn thread-group)))
on-click
(mf/use-fn
(mf/deps thread-group position)
(fn []
(let [updated-zoom (calculate-zoom-scale-to-ungroup-bubbles zoom thread-group)
scale-zoom (/ updated-zoom zoom)]
(st/emit! (dwz/set-zoom position scale-zoom)))))]
[:div {:style {:top (dm/str pos-y "px")
:left (dm/str pos-x "px")}
:on-click on-click
:class (stl/css :floating-preview-wrapper :floating-preview-bubble)}
[:> comment-avatar*
{:class (stl/css :avatar-lg)
:variant (if unread? "unread" "read")
:data-testid (dm/str "floating-thread-bubble-" test-id)}
num-threads]]))
(mf/defc comment-floating-bubble*
{::mf/wrap [mf/memo]}
[{:keys [thread zoom is-open on-click origin position-modifier]}]
(let [owner (mf/with-memo [thread]
(dcm/get-owner thread))
base-pos (cond-> (:position thread)
position (:position thread)
position (cond-> position
(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)
position (:position thread)
frame-id (:frame-id thread)
state (mf/use-state
@ -1097,8 +1162,14 @@
: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))
pos-x (floor (* (or (:new-position-x @state) (:x position)) zoom))
pos-y (floor (* (or (:new-position-y @state) (:y position)) zoom))
drag? (mf/use-ref nil)
was-open? (mf/use-ref nil)
dragging-ref (mf/use-ref false)
start-ref (mf/use-ref nil)
on-pointer-down
(mf/use-fn

View file

@ -91,6 +91,17 @@
border-radius: $br-circle;
}
.avatar-text {
border-radius: $br-circle;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: $fs-12;
background-color: var(--color-background-quaternary);
}
.avatar-mask {
border-radius: $br-circle;
position: absolute;

View file

@ -62,7 +62,7 @@
on-navigate
(mf/use-callback
(fn [thread]
(st/emit! (-> (dwcm/navigate thread)
(st/emit! (-> (dwcm/navigate-to-comment-from-dashboard thread)
(with-meta {::ev/origin "dashboard"})))))
on-read-all

View file

@ -16,6 +16,7 @@
[rumext.v2 :as mf]))
(mf/defc comments-layer*
{::mf/wrap [mf/memo]}
[{:keys [vbox vport zoom file-id page-id]}]
(let [vbox-x (dm/get-prop vbox :x)
vbox-y (dm/get-prop vbox :y)
@ -59,11 +60,18 @@
:height (dm/str vport-h "px")}}
[:div {:class (stl/css :threads)
:style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}}
(for [item threads]
[:> cmt/comment-floating-bubble* {:thread item
:zoom zoom
:is-open (= (:id item) (:open local))
:key (:seqn item)}])
(for [thread-group (cmt/group-bubbles zoom threads)]
(let [group? (> (count thread-group) 1)
thread (first thread-group)]
(if group?
[:> cmt/comment-floating-group* {:thread-group thread-group
:zoom zoom
:key (:seqn thread)}]
[:> cmt/comment-floating-bubble* {:thread thread
:zoom zoom
:is-open (= (:id thread) (:open local))
:key (:seqn thread)}])))
(when-let [id (:open local)]
(when-let [thread (get threads-map id)]