Fix favorites timeline not showing events when switching

The favorites timeline was empty because:
1. The @StateObject filter in InnerTimelineView was captured once at init
2. Favorite events were mixed with follows events and got drowned out

Fixed by:
- Adding viewId parameter to TimelineView to force view recreation on switch
- Creating separate favoriteEvents EventHolder for favorites
- Adding dedicated subscribe_to_favorites() subscription that inserts
  directly into favoriteEvents when contact cards are loaded
This commit is contained in:
William Casarin
2026-02-06 16:05:29 -08:00
committed by Daniel D’Aquino
parent 9a1ae6f9b5
commit 434c54f98e
4 changed files with 53 additions and 23 deletions
+1 -1
View File
@@ -461,7 +461,7 @@ struct ContentView: View {
self.active_full_screen_item = item
}
.onReceive(handle_notify(.favoriteUpdated)) { _ in
home.resubscribe(.following)
home.subscribe_to_favorites()
}
.onReceive(handle_notify(.zapping)) { zap_ev in
guard !zap_ev.is_custom else {
+39 -13
View File
@@ -68,6 +68,7 @@ class HomeModel: ContactsDelegate, ObservableObject {
var should_debounce_dms = true
var homeHandlerTask: Task<Void, Never>?
var favoritesHandlerTask: Task<Void, Never>?
var notificationsHandlerTask: Task<Void, Never>?
var generalHandlerTask: Task<Void, Never>?
var dmsHandlerTask: Task<Void, Never>?
@@ -81,6 +82,7 @@ class HomeModel: ContactsDelegate, ObservableObject {
var notifications = NotificationsModel()
var notification_status = NotificationStatusModel()
var events: EventHolder = EventHolder()
var favoriteEvents: EventHolder = EventHolder()
var already_reposted: Set<NoteId> = Set()
var zap_button: ZapButtonModel = ZapButtonModel()
@@ -692,18 +694,6 @@ class HomeModel: ContactsDelegate, ObservableObject {
home_filters.append(hashtag_filter)
}
// Add filter for favorited users who we dont follow
if damus_state.settings.enable_favourites_feature {
let all_favorites = damus_state.contactCards.favorites
let favorited_not_followed = Array(all_favorites.subtracting(Set(friends)))
if !favorited_not_followed.isEmpty {
var favorites_filter = NostrFilter(kinds: home_filter_kinds)
favorites_filter.authors = favorited_not_followed
favorites_filter.limit = 500
home_filters.append(favorites_filter)
}
}
self.homeHandlerTask?.cancel()
self.homeHandlerTask = Task {
let startTime = CFAbsoluteTimeGetCurrent()
@@ -742,7 +732,43 @@ class HomeModel: ContactsDelegate, ObservableObject {
}
}
}
/// Subscribe to favorites - called when contact cards are loaded or favorites change
func subscribe_to_favorites() {
guard damus_state.settings.enable_favourites_feature else { return }
let all_favorites = Array(damus_state.contactCards.favorites)
guard !all_favorites.isEmpty else { return }
var home_filter_kinds: [NostrKind] = [.text, .longform, .boost, .highlight]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)
}
var favorites_filter = NostrFilter(kinds: home_filter_kinds)
favorites_filter.authors = all_favorites
favorites_filter.limit = 500
self.favoritesHandlerTask?.cancel()
self.favoritesHandlerTask = Task {
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [favorites_filter], streamMode: .ndbAndNetworkParallel(networkOptimization: .sinceOptimization)) {
switch item {
case .event(let lender):
await lender.justUseACopy({ await self.insert_favorite_event($0) })
case .eose, .ndbEose, .networkEose:
break
}
}
}
}
@MainActor
func insert_favorite_event(_ ev: NostrEvent) {
guard should_show_event(state: damus_state, ev: ev) else { return }
damus_state.events.insert(ev)
favoriteEvents.insert(ev)
}
/// Adapter pattern to make migration easier
enum SubscriptionContext {
case home
@@ -52,21 +52,21 @@ struct PostingTimelineView: View {
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter)
// If favourites feature is disabled, always use follows
let sourceToUse = damus_state.settings.enable_favourites_feature ? timeline_source : .follows
switch sourceToUse {
case .follows:
// Only apply friend_filter for follows timeline
// Favorites timeline uses a dedicated EventHolder (favoriteEvents) that already contains only favorited users' events
if sourceToUse == .follows {
filters.append(damus_state.contacts.friend_filter)
case .favorites:
filters.append(damus_state.contactCards.filter)
}
return ContentFilters(filters: filters).filter
}
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
TimelineView<AnyView>(events: home.events, loading: self.loading, headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter)
let eventsSource = timeline_source == .favorites ? home.favoriteEvents : home.events
return TimelineView<AnyView>(events: eventsSource, loading: self.loading, headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter, viewId: timeline_source)
}
func HeaderView() -> some View {
@@ -21,8 +21,9 @@ struct TimelineView<Content: View>: View {
let filter: (NostrEvent) -> Bool
let content: Content?
let apply_mute_rules: Bool
let viewId: AnyHashable?
init(events: EventHolder, loading: Binding<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
init(events: EventHolder, loading: Binding<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, viewId: AnyHashable? = nil, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self._headerHeight = headerHeight
@@ -31,10 +32,11 @@ struct TimelineView<Content: View>: View {
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.viewId = viewId
self.content = content?()
}
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, viewId: AnyHashable? = nil, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self._headerHeight = .constant(0.0)
@@ -43,6 +45,7 @@ struct TimelineView<Content: View>: View {
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.viewId = viewId
self.content = content?()
}
@@ -71,6 +74,7 @@ struct TimelineView<Content: View>: View {
.frame(height: 0)
InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.id(viewId)
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.disabled(loading)