From b90ea45b8dd538b4149a5ead2542531caf065e57 Mon Sep 17 00:00:00 2001 From: Bryan Montz Date: Tue, 31 Dec 2024 07:32:02 -0600 Subject: [PATCH] added remembering which feed source is selected --- CHANGELOG.md | 1 + Nos/Controller/FeedController.swift | 88 +++++++++++++++++++----- Nos/Models/CoreData/Event+Fetching.swift | 5 +- Nos/Views/Home/FeedPicker.swift | 45 +++++++----- Nos/Views/Home/HomeFeedView.swift | 3 +- 5 files changed, 102 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c613c67d4..31dce629e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add empty state for lists/relays drop-down. - Added support for decrypting private tags in kind 30000 lists. - Added pop-up tip for feed customization. [#101](https://github.com/verse-pbc/issues/issues/101) +- Added remembering which feed source is selected. ### Internal Changes - Upgraded to Xcode 16. [#1570](https://github.com/planetary-social/nos/issues/1570) diff --git a/Nos/Controller/FeedController.swift b/Nos/Controller/FeedController.swift index 5d4464f67..e07316273 100644 --- a/Nos/Controller/FeedController.swift +++ b/Nos/Controller/FeedController.swift @@ -4,7 +4,7 @@ import Dependencies import SwiftUI /// The source to be used for a feed of notes. -enum FeedSource: Hashable, Equatable { +enum FeedSource: RawRepresentable, Hashable, Equatable { case following case relay(String, String?) case list(String, String?) @@ -31,6 +31,44 @@ enum FeedSource: Hashable, Equatable { default: false } } + + // Note: RawRepresentable conformance is required for use of @AppStorage for persistence. + var rawValue: String { + switch self { + case .following: + "following" + case .relay(let host, let description): + "relay:|\(host):|\(description ?? "")" + case .list(let name, let description): + "list:|\(name):|\(description ?? "")" + } + } + + init?(rawValue: String) { + let components = rawValue.split(separator: ":|").map { String($0) } + guard let caseName = components.first else { + return nil + } + + switch caseName { + case "following": + self = .following + case "relay": + guard components.count >= 2 else { + return nil + } + let description = components.count >= 3 ? components[2] : "" + self = .relay(components[1], description) + case "list": + guard components.count >= 2 else { + return nil + } + let description = components.count >= 3 ? components[2] : "" + self = .list(components[1], description) + default: + return nil + } + } } @Observable @MainActor final class FeedController { @@ -42,24 +80,13 @@ enum FeedSource: Hashable, Equatable { private(set) var selectedList: AuthorList? private(set) var selectedRelay: Relay? - var selectedSource: FeedSource = .following { + + @ObservationIgnored @AppStorage("selectedFeedSource") private var persistedSelectedSource = FeedSource.following + + var selectedSource = FeedSource.following { didSet { - switch selectedSource { - case .relay(let address, _): - if let relay = relays.first(where: { $0.host == address }) { - selectedRelay = relay - selectedList = nil - } - case .list(let title, _): - // TODO: Needs to use replaceableID instead of title - if let list = lists.first(where: { $0.title == title }) { - selectedList = list - selectedRelay = nil - } - default: - selectedList = nil - selectedRelay = nil - } + updateSelectedListOrRelay() + persistedSelectedSource = selectedSource } } @@ -82,6 +109,12 @@ enum FeedSource: Hashable, Equatable { init() { observeLists() observeRelays() + + // The delay here is an unfortunate workaround. Without it, the feed always resumes to + // the default value of FeedSource.following. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { + self.selectedSource = self.persistedSelectedSource + } } private func observeLists() { @@ -136,6 +169,25 @@ enum FeedSource: Hashable, Equatable { .store(in: &cancellables) } + private func updateSelectedListOrRelay() { + switch selectedSource { + case .relay(let address, _): + if let relay = relays.first(where: { $0.host == address }) { + selectedRelay = relay + selectedList = nil + } + case .list(let title, _): + // TODO: Needs to use replaceableID instead of title + if let list = lists.first(where: { $0.title == title }) { + selectedList = list + selectedRelay = nil + } + default: + selectedList = nil + selectedRelay = nil + } + } + private func updateEnabledSources() { var enabledSources = [FeedSource]() enabledSources.append(.following) diff --git a/Nos/Models/CoreData/Event+Fetching.swift b/Nos/Models/CoreData/Event+Fetching.swift index 6bd48be25..82d751400 100644 --- a/Nos/Models/CoreData/Event+Fetching.swift +++ b/Nos/Models/CoreData/Event+Fetching.swift @@ -392,11 +392,12 @@ extension Event { @nonobjc public class func homeFeed( for user: Author, after: Date, - seenOn relay: Relay? = nil + seenOn relay: Relay? = nil, + from authors: Set? = nil ) -> NSFetchRequest { let fetchRequest = NSFetchRequest(entityName: "Event") fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)] - fetchRequest.predicate = homeFeedPredicate(for: user, after: after, seenOn: relay) + fetchRequest.predicate = homeFeedPredicate(for: user, after: after, seenOn: relay, from: authors) return fetchRequest } diff --git a/Nos/Views/Home/FeedPicker.swift b/Nos/Views/Home/FeedPicker.swift index 127f963d9..c0530142e 100644 --- a/Nos/Views/Home/FeedPicker.swift +++ b/Nos/Views/Home/FeedPicker.swift @@ -7,28 +7,35 @@ struct FeedPicker: View { var body: some View { BeveledContainerView { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 0) { - ForEach(feedController.enabledSources, id: \.self) { source in - Button(action: { - withAnimation(nil) { - feedController.selectedSource = source - } - }, label: { - let isSelected = feedController.selectedSource == source - Text(source.displayName) - .font(.system(size: 16, weight: isSelected ? .medium : .regular)) - .padding(.horizontal, 10) - .padding(.vertical, 4) - .background(isSelected ? Color.pickerBackgroundSelected : Color.clear) - .foregroundStyle(isSelected ? Color.white : Color.secondaryTxt) - .clipShape(Capsule()) - }) + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 0) { + ForEach(feedController.enabledSources, id: \.self) { source in + Button(action: { + withAnimation(nil) { + feedController.selectedSource = source + } + }, label: { + let isSelected = feedController.selectedSource == source + Text(source.displayName) + .font(.system(size: 16, weight: isSelected ? .medium : .regular)) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(isSelected ? Color.pickerBackgroundSelected : Color.clear) + .foregroundStyle(isSelected ? Color.white : Color.secondaryTxt) + .clipShape(Capsule()) + }) + } + } + .padding(.horizontal, 8) + .onChange(of: feedController.selectedSource) { + withAnimation { + proxy.scrollTo(feedController.selectedSource) + } } } - .padding(.horizontal, 8) + .frame(height: 40) } - .frame(height: 40) } .background(Color.cardBgTop) } diff --git a/Nos/Views/Home/HomeFeedView.swift b/Nos/Views/Home/HomeFeedView.swift index 744865716..5e8007a35 100644 --- a/Nos/Views/Home/HomeFeedView.swift +++ b/Nos/Views/Home/HomeFeedView.swift @@ -48,7 +48,8 @@ struct HomeFeedView: View { Event.homeFeed( for: user, after: refreshController.lastRefreshDate, - seenOn: feedController.selectedRelay + seenOn: feedController.selectedRelay, + from: feedController.selectedList?.allAuthors ) }