From 81251ee88a6d8ea94ff23b7367e64d4547cdd188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 5 Jan 2026 19:14:12 -0800 Subject: [PATCH] Fix app freeze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes an issue where the app would occasionally freeze. The filtered holders were being initialized and registered directly from a SwiftUI initializer, which would sometimes cause hundreds of instances to be initialized and registered and never removed by `onDisappear`. The issue was fixed by initializing such objects with `StateObject`, which brings it a more stable identity that lives as long as the SwiftUI view it is in, and by placing the init/deinit registration/clean-up logic in the filtered holder object itself, better matching the lifecycle and preventing resource leakage. Changelog-Fixed: Fixed an issue that would occasionally cause the app to freeze Closes: https://github.com/damus-io/damus/issues/3383 Signed-off-by: Daniel D’Aquino --- .../Timeline/Views/InnerTimelineView.swift | 12 ++------ damus/Shared/Utilities/EventHolder.swift | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/damus/Features/Timeline/Views/InnerTimelineView.swift b/damus/Features/Timeline/Views/InnerTimelineView.swift index 51c8f82d..a91b0a61 100644 --- a/damus/Features/Timeline/Views/InnerTimelineView.swift +++ b/damus/Features/Timeline/Views/InnerTimelineView.swift @@ -10,17 +10,14 @@ import SwiftUI struct InnerTimelineView: View { var events: EventHolder - @ObservedObject var filteredEvents: EventHolder.FilteredHolder - var filteredEventHolderId: UUID + @StateObject var filteredEvents: EventHolder.FilteredHolder let state: DamusState init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) { self.events = events self.state = damus let filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter - let filteredEvents = EventHolder.FilteredHolder(filter: filter) - self.filteredEvents = filteredEvents - self.filteredEventHolderId = events.add(filteredHolder: filteredEvents) + _filteredEvents = StateObject.init(wrappedValue: EventHolder.FilteredHolder(filter: filter, parent: events)) } var event_options: EventViewOptions { @@ -65,11 +62,6 @@ struct InnerTimelineView: View { } } } - .onDisappear { - self.events.removeFilteredHolder(id: self.filteredEventHolderId) - } - //.padding(.horizontal) - } } diff --git a/damus/Shared/Utilities/EventHolder.swift b/damus/Shared/Utilities/EventHolder.swift index 24026167..b6fd34f6 100644 --- a/damus/Shared/Utilities/EventHolder.swift +++ b/damus/Shared/Utilities/EventHolder.swift @@ -56,16 +56,20 @@ class EventHolder: ObservableObject, ScrollQueue { has_event.insert(ev.id) + var changed = false + if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) { - return true + changed = true } - for (id, filteredView) in self.filteredHolders { + + for (_, filteredView) in self.filteredHolders { filteredView.insert(event: ev) } - return false + return changed } + @MainActor private func insert_queued(_ ev: NostrEvent) -> Bool { if has_event.contains(ev.id) { return false @@ -79,6 +83,7 @@ class EventHolder: ObservableObject, ScrollQueue { return true } + @MainActor func flush() { guard !incoming.isEmpty else { return @@ -89,7 +94,7 @@ class EventHolder: ObservableObject, ScrollQueue { if insert_uniq_sorted_event_created(events: &events, new_ev: event) { changed = true } - for (id, filteredHolder) in self.filteredHolders { + for (_, filteredHolder) in self.filteredHolders { filteredHolder.insert(event: event) } } @@ -107,7 +112,7 @@ class EventHolder: ObservableObject, ScrollQueue { func reset() { self.incoming = [] self.events = [] - for (id, filteredHolder) in filteredHolders { + for (_, filteredHolder) in filteredHolders { filteredHolder.update(events: []) } } @@ -125,13 +130,26 @@ class EventHolder: ObservableObject, ScrollQueue { self.filteredHolders[id] = nil } + @MainActor class FilteredHolder: ObservableObject { @Published private(set) var events: [NostrEvent] let filter: (NostrEvent) -> Bool + private var id: UUID? + private weak var parent: EventHolder? - init(filter: @escaping (NostrEvent) -> Bool) { + init(filter: @escaping (NostrEvent) -> Bool, parent: EventHolder) { self.events = [] self.filter = filter + self.parent = parent + self.id = parent.add(filteredHolder: self) + } + + deinit { + // Capture for async cleanup + guard let id = id, let parent = parent else { return } + Task { @MainActor in + parent.removeFilteredHolder(id: id) + } } func update(events: [NostrEvent]) {