Merge pull request #3708 from askeew/favorites-fix-xcode
Fix favorites timeline
This commit is contained in:
@@ -131,6 +131,7 @@ struct ContentView: View {
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@State private var isSideBarOpened = false
|
||||
@State var headerOffset: CGFloat = 0.0
|
||||
@State private var postingTimelineSource: TimelineSource = .follows
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||
@@ -188,7 +189,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
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:
|
||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||
|
||||
@@ -15,18 +15,23 @@ struct FavoriteButtonView: View {
|
||||
var body: some View {
|
||||
Button(
|
||||
action: {
|
||||
guard let keypair = damus_state.keypair.to_full() else { return }
|
||||
|
||||
damus_state.contactCards.toggleFavorite(
|
||||
pubkey,
|
||||
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")
|
||||
.foregroundColor(favorite ? DamusColors.purple : .primary)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.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 events: 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 zap_button: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
@@ -738,7 +742,17 @@ class HomeModel: ContactsDelegate, ObservableObject {
|
||||
guard damus_state.settings.enable_favourites_feature else { return }
|
||||
|
||||
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]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
@@ -749,24 +763,67 @@ class HomeModel: ContactsDelegate, ObservableObject {
|
||||
favorites_filter.authors = all_favorites
|
||||
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 = 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 {
|
||||
case .event(let lender):
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter pattern to make migration easier
|
||||
|
||||
@@ -28,7 +28,7 @@ struct PostingTimelineView: View {
|
||||
@State var headerHeight: CGFloat = 0
|
||||
@Binding var headerOffset: CGFloat
|
||||
@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? = {
|
||||
if #available(iOS 18.0, *) {
|
||||
@@ -187,7 +187,8 @@ struct PostingTimelineView_Previews: PreviewProvider {
|
||||
homeEvents: .init(),
|
||||
isSideBarOpened: .constant(false),
|
||||
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
|
||||
}
|
||||
|
||||
/// Filters stored events in place and rebuilds dedup state.
|
||||
/// Note: callers are responsible for refreshing `filteredHolders` afterwards.
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||
self.events = self.events.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
|
||||
|
||||
Reference in New Issue
Block a user