Skip to content

Commit

Permalink
Merge pull request #1720 from planetary-social/feature/lists-ui
Browse files Browse the repository at this point in the history
Lists UI
  • Loading branch information
mplorentz authored Jan 3, 2025
2 parents b641d65 + 8713a6a commit b97acbf
Show file tree
Hide file tree
Showing 39 changed files with 1,493 additions and 114 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Nos now publishes the hashtags it finds in your note when you post. This means it works the way you’ve always expected it to work. [#44](https://github.com/verse-pbc/issues/issues/44)
- Fixed crash related to tracking delete events. [#96](https://github.com/verse-pbc/issues/issues/96)
- Updated the default relays that are added when you create an account. [#17](https://github.com/verse-pbc/issues/issues/17)
- Added feed picker view (UI only). [#103](https://github.com/verse-pbc/issues/issues/103)
- Added feed source customizer drop-down view. [#102](https://github.com/verse-pbc/issues/issues/102)
- Make feed source selector work.
- 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.
- Factored out the segmented picker on the ProfileHeader for reusability.
- Fixed a case where lists don't show up immediately after signing in.
- Fixed a minor cell layout issue on feed customizer drop-down view.
- Fixed issue where feed shows following content rather than selected list after app restart. [#114](https://github.com/verse-pbc/issues/issues/114)
- Update relays icon on the feed customizer view to match the one in side menu.

### Internal Changes
- Download and parse an author’s lists when viewing their profile. [#49](https://github.com/verse-pbc/issues/issues/49)
Expand Down
52 changes: 51 additions & 1 deletion Nos.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x18",
"green" : "0x18",
"red" : "0x18"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
20 changes: 20 additions & 0 deletions Nos/Assets/Colors.xcassets/tip-shadow.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2D",
"green" : "0x39",
"red" : "0xAA"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
178 changes: 178 additions & 0 deletions Nos/Controller/FeedController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import Combine
import CoreData
import Dependencies
import SwiftUI

@Observable @MainActor final class FeedController {

@ObservationIgnored @Dependency(\.persistenceController) private var persistenceController

let author: Author

var enabledSources: [FeedSource] = [.following]

private(set) var selectedList: AuthorList?
private(set) var selectedRelay: Relay?

@ObservationIgnored @AppStorage("selectedFeedSource") private var persistedSelectedSource = FeedSource.following

var selectedSource = FeedSource.following {
didSet {
updateSelectedListOrRelay()
persistedSelectedSource = selectedSource
}
}

private(set) var listRowItems: [FeedToggleRow.Item] = []
private(set) var relayRowItems: [FeedToggleRow.Item] = []

private var lists: [AuthorList] = [] {
didSet {
updateEnabledSources()
}
}
private var relays: [Relay] = [] {
didSet {
updateEnabledSources()
}
}

private var cancellables = Set<AnyCancellable>()

init(author: Author) {
self.author = author
observeLists()
observeRelays()

// TODO: I commented this code out because it wasn't fixing the bug it was intended to yet. Let's come back to
// it. https://github.com/planetary-social/nos/pull/1720#issuecomment-2569529771
// 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(1500)) {
// self.selectedSource = self.persistedSelectedSource
// }
}

private func observeLists() {
let request = NSFetchRequest<AuthorList>(entityName: "AuthorList")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)]
request.predicate = NSPredicate(
format: "kind = %i AND author = %@ AND title != nil",
EventKind.followSet.rawValue,
author
)

let listWatcher = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: persistenceController.viewContext,
sectionNameKeyPath: nil,
cacheName: "FeedController.listWatcher"
)

FetchedResultsControllerPublisher(fetchedResultsController: listWatcher)
.publisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] lists in
self?.lists = lists
})
.store(in: &cancellables)
}

private func observeRelays() {
let request = Relay.relays(for: author)

let relayWatcher = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: persistenceController.viewContext,
sectionNameKeyPath: nil,
cacheName: "FeedController.relayWatcher"
)

FetchedResultsControllerPublisher(fetchedResultsController: relayWatcher)
.publisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] relays in
self?.relays = relays
})
.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)

var listItems = [FeedToggleRow.Item]()
var relayItems = [FeedToggleRow.Item]()

for list in lists {
let source = FeedSource.list(name: list.title ?? "??", description: nil)

if list.isFeedEnabled {
enabledSources.append(source)
}

listItems.append(FeedToggleRow.Item(source: source, isOn: list.isFeedEnabled))
}

for relay in relays {
let source = FeedSource.relay(host: relay.host ?? "", description: relay.relayDescription)

if relay.isFeedEnabled {
enabledSources.append(source)
}

relayItems.append(FeedToggleRow.Item(source: source, isOn: relay.isFeedEnabled))
}

self.enabledSources = enabledSources
self.listRowItems = listItems
self.relayRowItems = relayItems
}

func toggleSourceEnabled(_ source: FeedSource) {
do {
switch source {
case .relay(let address, _):
if let relay = relays.first(where: { $0.host == address }) {
relay.isFeedEnabled.toggle()
try relay.managedObjectContext?.save()
updateEnabledSources()
}
case .list(let title, _):
// TODO: Needs to use replaceableID instead of title
if let list = lists.first(where: { $0.title == title }) {
list.isFeedEnabled.toggle()
try list.managedObjectContext?.save()
updateEnabledSources()
}
default:
break
}
} catch {
print("FeedController: error updating source: \(source), error: \(error)")
}
}

func isSourceEnabled(_ source: FeedSource) -> Bool {
enabledSources.contains(source)
}
}
2 changes: 1 addition & 1 deletion Nos/Controller/FetchRequestPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Combine
import CoreData

/// Create by passing in a FetchedResultsController
/// This will perform the fetch request on the correct queue and publish the resutls on the
/// This will perform the fetch request on the correct queue and publish the results on the
/// publishers.
/// source: https://gist.github.com/josephlord/0d6a9d0871bd2e1b3a3bdbf20c184f88
///
Expand Down
33 changes: 32 additions & 1 deletion Nos/Models/CoreData/AuthorList+CoreDataClass.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import Foundation
import CoreData
import NostrSDK

/// This class is needed only as a utility. The protocol functions only work on instances,
/// (as opposed to classes in static functions).
fileprivate final class TagInterpreter: PrivateTagInterpreting, DirectMessageEncrypting {
}

extension Keypair {
static func withNosKeyPair(_ keyPair: KeyPair) -> Keypair? {
Keypair(nsec: keyPair.nsec)
}
}

@objc(AuthorList)
public class AuthorList: Event {
static func createOrUpdate(
from jsonEvent: JSONEvent,
keyPair: KeyPair? = nil,
in context: NSManagedObjectContext
) throws -> AuthorList {
guard jsonEvent.kind == EventKind.followSet.rawValue else { throw AuthorListError.invalidKind }
Expand Down Expand Up @@ -45,7 +58,21 @@ public class AuthorList: Event {
authorList.listDescription = tag[safe: 1]
}
}


if !jsonEvent.content.isEmpty,
let keyPair,
let nostrSDKKeypair = Keypair.withNosKeyPair(keyPair) {
let authorIDs = TagInterpreter().valuesForPrivateTags(
from: jsonEvent.content,
withName: .pubkey,
using: nostrSDKKeypair
)
for authorID in authorIDs {
let author = try Author.findOrCreate(by: authorID, context: context)
authorList.addToPrivateAuthors(author)
}
}

return authorList
}

Expand All @@ -65,4 +92,8 @@ public class AuthorList: Event {
fetchRequest.fetchLimit = 1
return fetchRequest
}

var allAuthors: Set<Author> {
authors.union(privateAuthors)
}
}
19 changes: 14 additions & 5 deletions Nos/Models/CoreData/Event+Fetching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ extension Event {
for user: Author,
before: Date? = nil,
after: Date? = nil,
seenOn relay: Relay? = nil
seenOn relay: Relay? = nil,
from authors: Set<Author>? = nil
) -> NSPredicate {
let kind1Predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "kind = 1"),
Expand Down Expand Up @@ -366,29 +367,37 @@ extension Event {
NSPredicate(format: "(ANY author.followers.source = %@ OR author = %@)", user, user)
)
}

if let authors {
andPredicates.append(
NSPredicate(format: "author IN %@", authors)
)
}

return NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
}

@nonobjc public class func homeFeed(
for user: Author,
before: Date,
seenOn relay: Relay? = nil
seenOn relay: Relay? = nil,
from authors: Set<Author>? = nil
) -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: "Event")
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: false)]
fetchRequest.predicate = homeFeedPredicate(for: user, before: before, seenOn: relay)
fetchRequest.predicate = homeFeedPredicate(for: user, before: before, seenOn: relay, from: authors)
return fetchRequest
}

@nonobjc public class func homeFeed(
for user: Author,
after: Date,
seenOn relay: Relay? = nil
seenOn relay: Relay? = nil,
from authors: Set<Author>? = nil
) -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(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
}

Expand Down
18 changes: 18 additions & 0 deletions Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ extension AuthorList {

/// The set of unique authors in this list.
@NSManaged public var authors: Set<Author>

/// The set of privately listed unique authors.
@NSManaged public var privateAuthors: Set<Author>

/// Whether or not this list should be visible in the ``FeedPicker``.
@NSManaged public var isFeedEnabled: Bool
}

// MARK: Generated accessors for authors
Expand All @@ -38,4 +44,16 @@ extension AuthorList {

@objc(removeAuthors:)
@NSManaged public func removeFromAuthors(_ values: NSSet)

@objc(addPrivateAuthorsObject:)
@NSManaged public func addToPrivateAuthors(_ value: Author)

@objc(removePrivateAuthorsObject:)
@NSManaged public func removeFromPrivateAuthors(_ value: Author)

@objc(addPrivateAuthors:)
@NSManaged public func addToPrivateAuthors(_ values: NSSet)

@objc(removePrivateAuthors:)
@NSManaged public func removeFromPrivateAuthors(_ values: NSSet)
}
2 changes: 2 additions & 0 deletions Nos/Models/CoreData/Generated/Relay+CoreDataProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ extension Relay {
@NSManaged public var events: Set<Event>
@NSManaged public var publishedEvents: Set<Event>
@NSManaged public var shouldBePublishedEvents: Set<Event>
/// Whether or not this relay should be visible in the ``FeedPicker``.
@NSManaged public var isFeedEnabled: Bool

// Metadata
@NSManaged public var name: String?
Expand Down
Loading

0 comments on commit b97acbf

Please sign in to comment.