Fix favorites timeline: filter in-place, manage queuing, persist tab selection and sync has_event.
Changelog-None: Signed-off-by: Askeew <askeew@hotmail.com>
This commit is contained in:
@@ -131,6 +131,7 @@ struct ContentView: View {
|
|||||||
@State var confirm_overwrite_mutelist: Bool = false
|
@State var confirm_overwrite_mutelist: Bool = false
|
||||||
@State private var isSideBarOpened = false
|
@State private var isSideBarOpened = false
|
||||||
@State var headerOffset: CGFloat = 0.0
|
@State var headerOffset: CGFloat = 0.0
|
||||||
|
@State private var postingTimelineSource: TimelineSource = .follows
|
||||||
var home: HomeModel = HomeModel()
|
var home: HomeModel = HomeModel()
|
||||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||||
@@ -186,7 +187,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView(damus_state: damus_state!, home: home, homeEvents: home.events, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset)
|
PostingTimelineView(damus_state: damus_state!, home: home, homeEvents: home.events, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset, timeline_source: $postingTimelineSource)
|
||||||
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||||
|
|||||||
@@ -15,18 +15,23 @@ struct FavoriteButtonView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Button(
|
Button(
|
||||||
action: {
|
action: {
|
||||||
|
guard let keypair = damus_state.keypair.to_full() else { return }
|
||||||
|
|
||||||
damus_state.contactCards.toggleFavorite(
|
damus_state.contactCards.toggleFavorite(
|
||||||
pubkey,
|
pubkey,
|
||||||
postbox: damus_state.nostrNetwork.postbox,
|
postbox: damus_state.nostrNetwork.postbox,
|
||||||
keyPair: damus_state.keypair.to_full()
|
keyPair: keypair
|
||||||
)
|
)
|
||||||
favorite.toggle()
|
favorite = damus_state.contactCards.isFavorite(pubkey)
|
||||||
}) {
|
}) {
|
||||||
Image(favorite ? "heart.fill" : "heart")
|
Image(favorite ? "heart.fill" : "heart")
|
||||||
.foregroundColor(favorite ? DamusColors.purple : .primary)
|
.foregroundColor(favorite ? DamusColors.purple : .primary)
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.system(size: 16, weight: .medium))
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.onReceive(handle_notify(.favoriteUpdated)) { _ in
|
||||||
|
favorite = damus_state.contactCards.isFavorite(pubkey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ class HomeModel: ContactsDelegate, ObservableObject {
|
|||||||
var notification_status = NotificationStatusModel()
|
var notification_status = NotificationStatusModel()
|
||||||
var events: EventHolder = EventHolder()
|
var events: EventHolder = EventHolder()
|
||||||
var favoriteEvents: EventHolder = EventHolder()
|
var favoriteEvents: EventHolder = EventHolder()
|
||||||
|
/// Tracks the set of favorited pubkeys from the last `subscribe_to_favorites()` call.
|
||||||
|
/// Used to detect when new authors are added so we can skip `sinceOptimization`
|
||||||
|
/// (ndb may lack their history). Cleared when favorites become empty.
|
||||||
|
private var previousFavorites: Set<Pubkey> = []
|
||||||
var already_reposted: Set<NoteId> = Set()
|
var already_reposted: Set<NoteId> = Set()
|
||||||
var zap_button: ZapButtonModel = ZapButtonModel()
|
var zap_button: ZapButtonModel = ZapButtonModel()
|
||||||
|
|
||||||
@@ -738,7 +742,17 @@ class HomeModel: ContactsDelegate, ObservableObject {
|
|||||||
guard damus_state.settings.enable_favourites_feature else { return }
|
guard damus_state.settings.enable_favourites_feature else { return }
|
||||||
|
|
||||||
let all_favorites = Array(damus_state.contactCards.favorites)
|
let all_favorites = Array(damus_state.contactCards.favorites)
|
||||||
guard !all_favorites.isEmpty else { return }
|
|
||||||
|
// Filter out events from users who are no longer favorites,
|
||||||
|
// keeping existing events for users who are still favorites.
|
||||||
|
self.favoriteEvents.filter { all_favorites.contains($0.pubkey) }
|
||||||
|
refreshFavoriteFilteredHolders()
|
||||||
|
|
||||||
|
guard !all_favorites.isEmpty else {
|
||||||
|
self.favoritesHandlerTask?.cancel()
|
||||||
|
previousFavorites = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var home_filter_kinds: [NostrKind] = [.text, .longform, .boost, .highlight]
|
var home_filter_kinds: [NostrKind] = [.text, .longform, .boost, .highlight]
|
||||||
if !damus_state.settings.onlyzaps_mode {
|
if !damus_state.settings.onlyzaps_mode {
|
||||||
@@ -749,24 +763,67 @@ class HomeModel: ContactsDelegate, ObservableObject {
|
|||||||
favorites_filter.authors = all_favorites
|
favorites_filter.authors = all_favorites
|
||||||
favorites_filter.limit = 500
|
favorites_filter.limit = 500
|
||||||
|
|
||||||
|
// Skip sinceOptimization when new authors were added, since ndb may not have
|
||||||
|
// their history and the `since` timestamp would be based on other authors' events.
|
||||||
|
let currentFavorites = Set(all_favorites)
|
||||||
|
let hasNewAuthors = !currentFavorites.isSubset(of: previousFavorites)
|
||||||
|
let streamMode: NostrNetworkManager.StreamMode =
|
||||||
|
hasNewAuthors
|
||||||
|
? .ndbAndNetworkParallel(networkOptimization: nil)
|
||||||
|
: .ndbAndNetworkParallel(networkOptimization: .sinceOptimization)
|
||||||
|
|
||||||
|
previousFavorites = currentFavorites
|
||||||
|
|
||||||
|
// Temporarily disable queuing so the initial batch of events from the
|
||||||
|
// new subscription goes directly into `events` instead of `incoming`.
|
||||||
|
// Once the initial load completes (eose), re-enable queuing.
|
||||||
|
// Capture the original state before any prior call may have disabled it,
|
||||||
|
// so rapid resubscriptions don't permanently leave queuing off.
|
||||||
|
let shouldRestoreQueuing = self.favoriteEvents.should_queue
|
||||||
|
self.favoriteEvents.set_should_queue(false)
|
||||||
|
|
||||||
self.favoritesHandlerTask?.cancel()
|
self.favoritesHandlerTask?.cancel()
|
||||||
self.favoritesHandlerTask = Task {
|
self.favoritesHandlerTask = Task {
|
||||||
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [favorites_filter], streamMode: .ndbAndNetworkParallel(networkOptimization: .sinceOptimization)) {
|
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [favorites_filter], streamMode: streamMode) {
|
||||||
switch item {
|
switch item {
|
||||||
case .event(let lender):
|
case .event(let lender):
|
||||||
await lender.justUseACopy({ await self.insert_favorite_event($0) })
|
await lender.justUseACopy({ await self.insert_favorite_event($0) })
|
||||||
case .eose, .ndbEose, .networkEose:
|
case .ndbEose:
|
||||||
|
self.refreshFavoriteFilteredHolders()
|
||||||
|
case .eose:
|
||||||
|
self.refreshFavoriteFilteredHolders()
|
||||||
|
// Only re-enable queuing after network EOSE so the full
|
||||||
|
// initial load (including network-only events) is visible.
|
||||||
|
if shouldRestoreQueuing {
|
||||||
|
self.favoriteEvents.set_should_queue(true)
|
||||||
|
}
|
||||||
|
case .networkEose:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates all filtered holders with the current favorite events.
|
||||||
|
@MainActor
|
||||||
|
private func refreshFavoriteFilteredHolders() {
|
||||||
|
for (_, filteredHolder) in favoriteEvents.filteredHolders {
|
||||||
|
filteredHolder.update(events: favoriteEvents.events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a favorite event into the holder and mirrors it to the global cache.
|
||||||
|
/// Rejects events from authors no longer in the favorites list (e.g. in-flight
|
||||||
|
/// events arriving after a favorite was removed and the stream cancelled).
|
||||||
@MainActor
|
@MainActor
|
||||||
func insert_favorite_event(_ ev: NostrEvent) {
|
func insert_favorite_event(_ ev: NostrEvent) {
|
||||||
guard should_show_event(state: damus_state, ev: ev) else { return }
|
guard should_show_event(state: damus_state, ev: ev) else { return }
|
||||||
|
guard damus_state.contactCards.favorites.contains(ev.pubkey) else { return }
|
||||||
|
// Only add to the global event cache if the event is new to the holder
|
||||||
|
let inserted = favoriteEvents.insert(ev)
|
||||||
|
if inserted {
|
||||||
damus_state.events.insert(ev)
|
damus_state.events.insert(ev)
|
||||||
favoriteEvents.insert(ev)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adapter pattern to make migration easier
|
/// Adapter pattern to make migration easier
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct PostingTimelineView: View {
|
|||||||
@State var headerHeight: CGFloat = 0
|
@State var headerHeight: CGFloat = 0
|
||||||
@Binding var headerOffset: CGFloat
|
@Binding var headerOffset: CGFloat
|
||||||
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||||
@State var timeline_source: TimelineSource = .follows
|
@Binding var timeline_source: TimelineSource
|
||||||
|
|
||||||
@State private var damusTips: Any? = {
|
@State private var damusTips: Any? = {
|
||||||
if #available(iOS 18.0, *) {
|
if #available(iOS 18.0, *) {
|
||||||
@@ -187,7 +187,8 @@ struct PostingTimelineView_Previews: PreviewProvider {
|
|||||||
homeEvents: .init(),
|
homeEvents: .init(),
|
||||||
isSideBarOpened: .constant(false),
|
isSideBarOpened: .constant(false),
|
||||||
active_sheet: .constant(nil),
|
active_sheet: .constant(nil),
|
||||||
headerOffset: .constant(0)
|
headerOffset: .constant(0),
|
||||||
|
timeline_source: .constant(.follows)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,13 @@ class EventHolder: ObservableObject, ScrollQueue {
|
|||||||
self.on_queue = on_queue
|
self.on_queue = on_queue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filters stored events in place and rebuilds dedup state.
|
||||||
|
/// Note: callers are responsible for refreshing `filteredHolders` afterwards.
|
||||||
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||||
self.events = self.events.filter(isIncluded)
|
self.events = self.events.filter(isIncluded)
|
||||||
self.incoming = self.incoming.filter(isIncluded)
|
self.incoming = self.incoming.filter(isIncluded)
|
||||||
|
// Rebuild has_event so removed events can be re-inserted later
|
||||||
|
self.has_event = Set(self.events.map(\.id) + self.incoming.map(\.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
Reference in New Issue
Block a user