From 44071e9d75885f4d1a0d916f87a94b5fba75ddd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 24 Nov 2025 17:52:46 -0800 Subject: [PATCH] Fix thread UI jumpiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 991a4a8, the `make_actionbar_model` function introduced an async call to populate the action bar data. This surfaced a pre-existing problem where the action bar model would reinstantiate in any SwiftUI render pass for the chat bubbles in `ChatroomThreadView`. This issue was not visible before because the whole computation happened directly on the main actor during the render, maintaining the illusion of a stable entity. Since the computation was moved to an async task (for performance and concurrency design reasons), it caused the action bar items to reload in each render pass, causing multiple re-renders and the jumpiness witnessed in the ticket. The issue was addressed by making the action bar model initialization happen within ChatEventView itself, and wrapping it on `StateObject` to make that entity stable across re-renders. This fixes an issue for an unreleased change, so no changelog entry is necessary. Changelog-None Fixes: 991a4a8 Closes: https://github.com/damus-io/damus/issues/3270 Signed-off-by: Daniel D’Aquino --- damus/Features/Chat/ChatEventView.swift | 29 ++++++++++++++------ damus/Features/Chat/ChatroomThreadView.swift | 3 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/damus/Features/Chat/ChatEventView.swift b/damus/Features/Chat/ChatEventView.swift index c7df2fbf..2a1798c1 100644 --- a/damus/Features/Chat/ChatEventView.swift +++ b/damus/Features/Chat/ChatEventView.swift @@ -36,9 +36,24 @@ struct ChatEventView: View { @State var selected_emoji: Emoji? @State private var isOnTopHalfOfScreen: Bool = false - @ObservedObject var bar: ActionBarModel + @StateObject private var bar: ActionBarModel @Environment(\.swipeViewGroupSelection) var swipeViewGroupSelection + init(event: NostrEvent, selected_event: NostrEvent, prev_ev: NostrEvent?, next_ev: NostrEvent?, damus_state: DamusState, thread: ThreadModel, scroll_to_event: ((_ id: NoteId) -> Void)?, focus_event: (() -> Void)?, highlight_bubble: Bool) { + self.event = event + self.selected_event = selected_event + self.prev_ev = prev_ev + self.next_ev = next_ev + self.damus_state = damus_state + self.thread = thread + self.scroll_to_event = scroll_to_event + self.focus_event = focus_event + self.highlight_bubble = highlight_bubble + + // Initialize @StateObject using wrappedValue + _bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus_state)) + } + enum PopoverState: String { case closed case open_emoji_selector @@ -340,21 +355,17 @@ struct ChatEventView: View { } #Preview { - let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state) - return ChatEventView(event: test_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar) + return ChatEventView(event: test_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false) } #Preview { - let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state) - return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar) + return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false) } #Preview { - let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state) - return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true, bar: bar) + return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true) } #Preview { - let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state) - return ChatEventView(event: test_super_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar) + return ChatEventView(event: test_super_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false) } diff --git a/damus/Features/Chat/ChatroomThreadView.swift b/damus/Features/Chat/ChatroomThreadView.swift index d2b50839..9bab7aec 100644 --- a/damus/Features/Chat/ChatroomThreadView.swift +++ b/damus/Features/Chat/ChatroomThreadView.swift @@ -64,8 +64,7 @@ struct ChatroomThreadView: View { focus_event: { self.set_active_event(scroller: scroller, ev: ev) }, - highlight_bubble: highlighted_note_id == ev.id, - bar: make_actionbar_model(ev: ev.id, damus: damus) + highlight_bubble: highlighted_note_id == ev.id ) .id(ev.id) .matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)