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 {
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)
}
}

View File

@@ -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]) {