Fix app freeze

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 <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2026-01-05 19:14:12 -08:00
parent cddee92f3a
commit 81251ee88a
2 changed files with 26 additions and 16 deletions

View File

@@ -10,17 +10,14 @@ import SwiftUI
struct InnerTimelineView: View { struct InnerTimelineView: View {
var events: EventHolder var events: EventHolder
@ObservedObject var filteredEvents: EventHolder.FilteredHolder @StateObject var filteredEvents: EventHolder.FilteredHolder
var filteredEventHolderId: UUID
let state: DamusState let state: DamusState
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) { init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
self.events = events self.events = events
self.state = damus self.state = damus
let filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter let filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
let filteredEvents = EventHolder.FilteredHolder(filter: filter) _filteredEvents = StateObject.init(wrappedValue: EventHolder.FilteredHolder(filter: filter, parent: events))
self.filteredEvents = filteredEvents
self.filteredEventHolderId = events.add(filteredHolder: filteredEvents)
} }
var event_options: EventViewOptions { var event_options: EventViewOptions {
@@ -65,11 +62,6 @@ struct InnerTimelineView: View {
} }
} }
} }
.onDisappear {
self.events.removeFilteredHolder(id: self.filteredEventHolderId)
}
//.padding(.horizontal)
} }
} }

View File

@@ -56,16 +56,20 @@ class EventHolder: ObservableObject, ScrollQueue {
has_event.insert(ev.id) has_event.insert(ev.id)
var changed = false
if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) { 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) filteredView.insert(event: ev)
} }
return false return changed
} }
@MainActor
private func insert_queued(_ ev: NostrEvent) -> Bool { private func insert_queued(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) { if has_event.contains(ev.id) {
return false return false
@@ -79,6 +83,7 @@ class EventHolder: ObservableObject, ScrollQueue {
return true return true
} }
@MainActor
func flush() { func flush() {
guard !incoming.isEmpty else { guard !incoming.isEmpty else {
return return
@@ -89,7 +94,7 @@ class EventHolder: ObservableObject, ScrollQueue {
if insert_uniq_sorted_event_created(events: &events, new_ev: event) { if insert_uniq_sorted_event_created(events: &events, new_ev: event) {
changed = true changed = true
} }
for (id, filteredHolder) in self.filteredHolders { for (_, filteredHolder) in self.filteredHolders {
filteredHolder.insert(event: event) filteredHolder.insert(event: event)
} }
} }
@@ -107,7 +112,7 @@ class EventHolder: ObservableObject, ScrollQueue {
func reset() { func reset() {
self.incoming = [] self.incoming = []
self.events = [] self.events = []
for (id, filteredHolder) in filteredHolders { for (_, filteredHolder) in filteredHolders {
filteredHolder.update(events: []) filteredHolder.update(events: [])
} }
} }
@@ -125,13 +130,26 @@ class EventHolder: ObservableObject, ScrollQueue {
self.filteredHolders[id] = nil self.filteredHolders[id] = nil
} }
@MainActor
class FilteredHolder: ObservableObject { class FilteredHolder: ObservableObject {
@Published private(set) var events: [NostrEvent] @Published private(set) var events: [NostrEvent]
let filter: (NostrEvent) -> Bool 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.events = []
self.filter = filter 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]) { func update(events: [NostrEvent]) {