eleven / messenger Public

mac: thread rows skip re-render when nothing they draw changed (#1071) (#1073)

* mac: thread rows skip re-render when nothing they draw changed (#1071)

The sidebar half of the render storm: per-pass Button/menu closures made
every thread row unprovable-as-unchanged, so any store tick re-ran every
row body (threadList → ThreadAvatar in the wedge samples). Equatable key
= thread + selection + rendered time label; == short-circuits the body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mac: ThreadRowCell constructs lazily — a skipped row builds nothing

Same amendment as FeedRow's: content becomes a closure so == skipping
skips value construction, not just body evaluation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 631ab753adfbce1534027b66f45f48f80e31ad60 parent 619f0b9
1 file changed, +29 −1
  • ios/LChatMac/NativeChatView.swift +29 −1
diff --git a/ios/LChatMac/NativeChatView.swift b/ios/LChatMac/NativeChatView.swift
index e53fb85..9278a2c 100644
--- a/ios/LChatMac/NativeChatView.swift
+++ b/ios/LChatMac/NativeChatView.swift
@@ -1223,7 +1223,11 @@ struct NativeChatView: View {
// one-click way back to everything.
if filtering { filterChip }
ForEach(visibleThreads) { thread in
- threadRow(thread)
+ ThreadRowCell(key: ThreadRowKey(thread: thread,
+ selected: thread.id == selectedID,
+ timeLabel: thread.lastAt > 0 ? relTime(thread.lastAt) : ""),
+ content: { AnyView(threadRow(thread)) })
+ .equatable()
}
// The funnel emptied the list — say so instead of presenting
// a silently blank pane. (Search's empty case is the panel's.)
@@ -1289,6 +1293,30 @@ struct NativeChatView: View {
store.reopenIfHidden(t) // opening a closed chat from search reopens it
}
+ // The sidebar's half of issue #1071 (the ClickLab wedge samples split
+ // between MessageBubble and threadList → ThreadAvatar): every store tick
+ // re-ran every thread row's body, because the per-pass Button/menu
+ // closures make rows unprovable-as-unchanged. The Equatable key lists
+ // exactly what a row draws — the thread itself, the selection highlight,
+ // and the rendered time label (so timestamps refresh exactly as often as
+ // the list re-evaluates, same as before the gate).
+ // ⚠️ Every render input must be listed — a field missing from the key
+ // means its changes stop repainting rows.
+ private struct ThreadRowKey: Equatable {
+ let thread: ChatThread
+ let selected: Bool
+ let timeLabel: String
+ }
+
+ private struct ThreadRowCell: View, Equatable {
+ let key: ThreadRowKey
+ // Lazy for the same reason as FeedRow: a skipped row must construct
+ // nothing, not just skip its body.
+ let content: () -> AnyView
+ static func == (a: Self, b: Self) -> Bool { a.key == b.key }
+ var body: some View { content() }
+ }
+
private func threadRow(_ t: ChatThread) -> some View {
Button {
selectThread(t)