perf: stop full annotation-list re-render on every bar crossing
commentary dereferenced active-annotation/active-annotations/active-note-set in its body, so the whole card list rebuilt (and React reconciled every card) on every playhead bar-crossing during playback and on context enter — the annotation-only jank + the enter "double flash". Push the per-item active state into granular per-id subs (::annotation-active? / ::note-active?): each card and each bound-note chip subscribes to its own state, so a crossing re-renders only the item(s) that flipped. The active-card scroll moves into a nil-rendering component that owns the active-annotation sub, so commentary no longer re-renders for it either. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d22bff7654
commit
05514f3522
2 changed files with 78 additions and 65 deletions
|
|
@ -204,3 +204,12 @@
|
|||
::active-note-set
|
||||
:<- [::note-bars] :<- [::playhead]
|
||||
(fn [[nbars ph] _] (gids-at nbars ph)))
|
||||
|
||||
;; per-id "is this one active right now" booleans. Subscribing these PER CARD /
|
||||
;; PER CHIP (instead of the whole set in the parent) means a bar crossing only
|
||||
;; re-renders the couple of items whose state actually flipped — not the entire
|
||||
;; annotation list. re-frame caches each by id and only notifies on a real change.
|
||||
(rf/reg-sub ::annotation-active? :<- [::active-annotations]
|
||||
(fn [s [_ id]] (contains? s id)))
|
||||
(rf/reg-sub ::note-active? :<- [::active-note-set]
|
||||
(fn [s [_ id]] (contains? s id)))
|
||||
|
|
|
|||
|
|
@ -897,23 +897,83 @@
|
|||
[:button {:on-click #(do (goto! lo true) (reset! open nil))}
|
||||
(str "▸ " (clip-at scene segs lo))])])])))
|
||||
|
||||
;; A bound-note chip on an annotation card. Owns its own »»» state so a bar
|
||||
;; crossing only re-renders the chip that flipped, not the whole card list.
|
||||
(defn- note-live-chip [ng n]
|
||||
(let [live? @(rf/subscribe [::subs/note-active? ng])]
|
||||
[:span.bound-note {:class (when live? "live")
|
||||
:title "Jump to this passage in the script"
|
||||
:on-click #(rf/dispatch [::events/jump-to-note ng])}
|
||||
(when live? [:span.note-live "»»» "])
|
||||
[:span.ann-swatch {:style {:background (:color n)}}]
|
||||
[:span.bound-note-name (:name n)]]))
|
||||
|
||||
;; One annotation card. Its "active" highlight is a PER-CARD subscription, so the
|
||||
;; playhead crossing bars re-renders only the card(s) whose active state flipped —
|
||||
;; not the entire commentary list (the old code derefed the active SET in the
|
||||
;; parent, forcing a full-list reconcile on every crossing).
|
||||
(defn- annotation-card [a scene ctx segs nmap authed? open]
|
||||
(let [active? @(rf/subscribe [::subs/annotation-active? (:id a)])]
|
||||
(if (:hidden a)
|
||||
(r/with-let [hov? (r/atom false)]
|
||||
[:div.ann {:id (str "ann-" (name (:id a)))
|
||||
:class (when active? "active")
|
||||
:style (merge {"--ann-color" (:color a)}
|
||||
{:opacity 0.4 :background "var(--desktop)" :background-size "2px 2px"
|
||||
:min-height "24px" :display "flex" :align-items "center"
|
||||
:justify-content "space-between" :padding "2px 6px"})
|
||||
:on-mouse-enter #(reset! hov? true)
|
||||
:on-mouse-leave #(reset! hov? false)}
|
||||
[:span.ann-title {:style {:font-size 10}}
|
||||
[:span.ann-swatch {:style {:background (:color a)}}]
|
||||
(:name a)]
|
||||
[:div {:style {:display "flex" :gap "2px" :visibility (if @hov? "visible" "hidden")}}
|
||||
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])} "⤢"]
|
||||
[:button.edit-btn {:title "Edit" :on-click #(rf/dispatch [::events/edit-draft (:id a)])} "✎"]]])
|
||||
[:div.ann {:id (str "ann-" (name (:id a)))
|
||||
:class (when active? "active")
|
||||
:style {"--ann-color" (:color a)}}
|
||||
[:div.ann-head
|
||||
[:div.ann-title
|
||||
[:span.ann-swatch {:style {:background (:color a)}}]
|
||||
(when (:broken a) [:span {:title (:reason a)} "△ "]) (:name a)]
|
||||
[:div.ann-actions
|
||||
[jump-control open a scene segs]
|
||||
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])}
|
||||
"⤢" (when (pos? (:nested a)) [:span.nest-badge (:nested a)])]
|
||||
(when authed?
|
||||
[:button.edit-btn {:title "Edit" :on-click #(rf/dispatch [::events/edit-draft (:id a)])} "✎"])
|
||||
(when authed?
|
||||
[:button.del-btn {:title "Delete"
|
||||
:on-click #(when (js/confirm (str "Delete \"" (:name a) "\"?"))
|
||||
(rf/dispatch [::events/delete-annotation (:id a)]))} "✕"])]]
|
||||
(when (not-empty (:content a)) [content-display scene ctx (:content a)])
|
||||
(when (seq (:notes a))
|
||||
[:div.ann-notes
|
||||
(for [ng (:notes a) :let [n (nmap ng)] :when n]
|
||||
^{:key (name ng)} [note-live-chip ng n])])])))
|
||||
|
||||
;; Renders nothing: owns the active-annotation subscription and scrolls the active
|
||||
;; card into view, so `commentary` itself no longer re-renders on every crossing.
|
||||
(defn- commentary-scroll [el]
|
||||
(let [active @(rf/subscribe [::subs/active-annotation])]
|
||||
(r/after-render
|
||||
(fn [] (when-let [c @el]
|
||||
(when-let [node (and active (.querySelector c (str "#ann-" (name active))))]
|
||||
(set! (.-scrollTop c) (max 0 (- (.-offsetTop node) (/ (.-clientHeight c) 3))))))))
|
||||
nil))
|
||||
|
||||
(defn commentary []
|
||||
(let [el (atom nil) open (r/atom nil)]
|
||||
(fn []
|
||||
(let [anns @(rf/subscribe [::subs/annotations])
|
||||
active @(rf/subscribe [::subs/active-annotation])
|
||||
active-set @(rf/subscribe [::subs/active-annotations])
|
||||
authed? @(rf/subscribe [::subs/authed?])
|
||||
scene @(rf/subscribe [::subs/scene])
|
||||
ctx @(rf/subscribe [::subs/context])
|
||||
segs @(rf/subscribe [::subs/segments])
|
||||
nmap (into {} (map (juxt :id identity)) @(rf/subscribe [::subs/notes]))
|
||||
live @(rf/subscribe [::subs/active-note-set])]
|
||||
(r/after-render
|
||||
(fn [] (when-let [c @el]
|
||||
(when-let [node (and active (.querySelector c (str "#ann-" (name active))))]
|
||||
(set! (.-scrollTop c) (max 0 (- (.-offsetTop node) (/ (.-clientHeight c) 3))))))))
|
||||
nmap (into {} (map (juxt :id identity)) @(rf/subscribe [::subs/notes]))]
|
||||
[:div.commentary {:ref (fn [n] (reset! el n))}
|
||||
[commentary-scroll el]
|
||||
;; this context's own description (links resolve in its parent), with an
|
||||
;; edit button — Edit drops into the parent timeline so marks are editable.
|
||||
(let [cg (get-in scene [:groups ctx])]
|
||||
|
|
@ -926,63 +986,7 @@
|
|||
(if (seq anns)
|
||||
(doall
|
||||
(for [a anns]
|
||||
(let [grp (get-in scene [:groups (:id a)])]
|
||||
^{:key (:id a)}
|
||||
(if (:hidden a)
|
||||
;; Hidden annotation: small collapsed card with edit/expand on hover
|
||||
(r/with-let [hov? (r/atom false)]
|
||||
[:div.ann {:id (str "ann-" (name (:id a)))
|
||||
:class (when (contains? active-set (:id a)) "active")
|
||||
:style (merge {"--ann-color" (:color a)}
|
||||
{:opacity 0.4
|
||||
:background "var(--desktop)"
|
||||
:background-size "2px 2px"
|
||||
:min-height "24px"
|
||||
:display "flex"
|
||||
:align-items "center"
|
||||
:justify-content "space-between"
|
||||
:padding "2px 6px"})
|
||||
:on-mouse-enter #(reset! hov? true)
|
||||
:on-mouse-leave #(reset! hov? false)}
|
||||
[:span.ann-title {:style {:font-size 10}}
|
||||
[:span.ann-swatch {:style {:background (:color a)}}]
|
||||
(:name a)]
|
||||
[:div {:style {:display "flex" :gap "2px" :visibility (if @hov? "visible" "hidden")}}
|
||||
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])}
|
||||
"⤢"]
|
||||
[:button.edit-btn {:title "Edit"
|
||||
:on-click #(rf/dispatch [::events/edit-draft (:id a)])}
|
||||
"✎"]]])
|
||||
;; Normal annotation: full card
|
||||
[:div.ann {:id (str "ann-" (name (:id a)))
|
||||
:class (when (contains? active-set (:id a)) "active")
|
||||
:style {"--ann-color" (:color a)}}
|
||||
[:div.ann-head
|
||||
[:div.ann-title
|
||||
[:span.ann-swatch {:style {:background (:color a)}}]
|
||||
(when (:broken a) [:span {:title (:reason a)} "△ "]) (:name a)]
|
||||
[:div.ann-actions
|
||||
[jump-control open a scene segs]
|
||||
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])}
|
||||
"⤢" (when (pos? (:nested a)) [:span.nest-badge (:nested a)])]
|
||||
(when authed?
|
||||
[:button.edit-btn {:title "Edit"
|
||||
:on-click #(rf/dispatch [::events/edit-draft (:id a)])} "✎"])
|
||||
(when authed?
|
||||
[:button.del-btn {:title "Delete"
|
||||
:on-click #(when (js/confirm (str "Delete \"" (:name a) "\"?"))
|
||||
(rf/dispatch [::events/delete-annotation (:id a)]))} "✕"])]]
|
||||
(when (not-empty (:content a)) [content-display scene ctx (:content a)])
|
||||
(when (seq (:notes a))
|
||||
[:div.ann-notes
|
||||
(for [ng (:notes a) :let [n (nmap ng)] :when n]
|
||||
^{:key (name ng)}
|
||||
[:span.bound-note {:class (when (contains? live ng) "live")
|
||||
:title "Jump to this passage in the script"
|
||||
:on-click #(rf/dispatch [::events/jump-to-note ng])}
|
||||
(when (contains? live ng) [:span.note-live "»»» "])
|
||||
[:span.ann-swatch {:style {:background (:color n)}}]
|
||||
[:span.bound-note-name (:name n)]])])]))))
|
||||
^{:key (:id a)} [annotation-card a scene ctx segs nmap authed? open]))
|
||||
[:div.ann-empty "No annotations here."])]))))
|
||||
|
||||
(defn- to-frame [v len]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue