Skip to content

Commit

Permalink
Merge pull request #1699 from planetary-social/lists
Browse files Browse the repository at this point in the history
Download and parse user lists
  • Loading branch information
joshuatbrown authored Dec 17, 2024
2 parents 45b90a4 + 8aa9d28 commit 6284443
Show file tree
Hide file tree
Showing 20 changed files with 640 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Internal Changes
- Upgraded to Xcode 16. [#1570](https://github.com/planetary-social/nos/issues/1570)
- Download and parse an author’s lists when viewing their profile. [#49](https://github.com/verse-pbc/issues/issues/49)

## [1.0.3] - 2024-12-04Z

Expand Down
44 changes: 40 additions & 4 deletions Nos.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Nos/Models/AuthorListError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// Errors for an ``AuthorList``.
enum AuthorListError: LocalizedError, Equatable {
/// The event kind is invalid; that is, an ``AuthorList`` can't be created with the given kind.
case invalidKind

/// The signature is invalid.
case invalidSignature(AuthorList)

/// The replaceable ID is missing (the `d` tag from the JSON event).
case missingReplaceableID
}
68 changes: 68 additions & 0 deletions Nos/Models/CoreData/AuthorList+CoreDataClass.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation
import CoreData

@objc(AuthorList)
public class AuthorList: Event {
static func createOrUpdate(
from jsonEvent: JSONEvent,
in context: NSManagedObjectContext
) throws -> AuthorList {
guard jsonEvent.kind == EventKind.followSet.rawValue else { throw AuthorListError.invalidKind }
guard let replaceableID = jsonEvent.replaceableID else { throw AuthorListError.missingReplaceableID }
let owner = try Author.findOrCreate(by: jsonEvent.pubKey, context: context)

// Fetch existing AuthorList if it exists
let fetchRequest = AuthorList.authorList(by: replaceableID, owner: owner, kind: EventKind.followSet.rawValue)
let existingAuthorList = try context.fetch(fetchRequest).first
existingAuthorList?.authors = Set()

let authorList = existingAuthorList ?? AuthorList(context: context)
authorList.createdAt = jsonEvent.createdDate
authorList.author = owner
authorList.owner = owner
authorList.identifier = jsonEvent.id
authorList.replaceableIdentifier = replaceableID
authorList.kind = jsonEvent.kind
authorList.signature = jsonEvent.signature
authorList.allTags = jsonEvent.tags as NSObject
authorList.content = jsonEvent.content

let tags = jsonEvent.tags

for tag in tags {
if tag[safe: 0] == "p", let authorID = tag[safe: 1] {
let author = try Author.findOrCreate(by: authorID, context: context)
authorList.addToAuthors(author)
} else if tag[safe: 0] == "title" {
authorList.title = tag[safe: 1]
} else if tag[safe: 0] == "image" {
if let urlString = tag[safe: 1] {
authorList.image = URL(string: urlString)
} else {
authorList.image = nil
}
} else if tag[safe: 0] == "description" {
authorList.listDescription = tag[safe: 1]
}
}

return authorList
}

@nonobjc public class func authorList(
by replaceableID: RawReplaceableID,
owner: Author,
kind: Int64
) -> NSFetchRequest<AuthorList> {
let fetchRequest = NSFetchRequest<AuthorList>(entityName: "AuthorList")
fetchRequest.predicate = NSPredicate(
format: "replaceableIdentifier = %@ AND owner = %@ AND kind = %i",
replaceableID,
owner,
kind
)
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AuthorList.identifier, ascending: true)]
fetchRequest.fetchLimit = 1
return fetchRequest
}
}
41 changes: 41 additions & 0 deletions Nos/Models/CoreData/Generated/AuthorList+CoreDataProperties.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation
import CoreData

extension AuthorList {

@nonobjc public class func fetchRequest() -> NSFetchRequest<AuthorList> {
NSFetchRequest<AuthorList>(entityName: "AuthorList")
}

/// The URL of an image representing the list.
@NSManaged public var image: URL?

/// The description of the list.
@NSManaged public var listDescription: String?

/// The title of the list.
@NSManaged public var title: String?

/// The owner of the list; the ``Author`` who created it.
/// Duplicates ``author`` but Core Data won't allow for multiple relationships to have the same inverse.
@NSManaged public var owner: Author?

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

// MARK: Generated accessors for authors
extension AuthorList {

@objc(addAuthorsObject:)
@NSManaged public func addToAuthors(_ value: Author)

@objc(removeAuthorsObject:)
@NSManaged public func removeFromAuthors(_ value: Author)

@objc(addAuthors:)
@NSManaged public func addToAuthors(_ values: NSSet)

@objc(removeAuthors:)
@NSManaged public func removeFromAuthors(_ values: NSSet)
}
2 changes: 1 addition & 1 deletion Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos 20.xcdatamodel</string>
<string>Nos 21.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Author" representedClassName=".Author" syncable="YES" codeGenerationType="category">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="hexadecimalPublicKey" attributeType="String"/>
<attribute name="lastUpdatedContactList" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastUpdatedMetadata" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="profilePhotoURL" optional="YES" attributeType="URI"/>
<attribute name="rawMetadata" optional="YES" attributeType="Binary"/>
<relationship name="events" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Event" inverseName="author" inverseEntity="Event"/>
<relationship name="followers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Follow" inverseName="destination" inverseEntity="Follow"/>
<relationship name="follows" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Follow" inverseName="source" inverseEntity="Follow"/>
<relationship name="relays" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
</entity>
<entity name="AuthorReference" representedClassName="AuthorReference" syncable="YES" codeGenerationType="category">
<attribute name="pubkey" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="event" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="authorReferences" inverseEntity="Event"/>
</entity>
<entity name="Event" representedClassName=".Event" syncable="YES" codeGenerationType="category">
<attribute name="allTags" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="kind" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sendAttempts" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="signature" optional="YES" attributeType="String"/>
<relationship name="author" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="events" inverseEntity="Author"/>
<relationship name="authorReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="AuthorReference" inverseName="event" inverseEntity="AuthorReference"/>
<relationship name="deletedOn" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="eventReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="EventReference" inverseName="referencingEvent" inverseEntity="EventReference"/>
<relationship name="publishedTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="referencingEvents" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="EventReference" inverseName="referencedEvent" inverseEntity="EventReference"/>
</entity>
<entity name="EventReference" representedClassName="EventReference" syncable="YES" codeGenerationType="category">
<attribute name="eventId" optional="YES" attributeType="String"/>
<attribute name="marker" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="referencedEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="referencingEvents" inverseEntity="Event"/>
<relationship name="referencingEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="eventReferences" inverseEntity="Event"/>
</entity>
<entity name="Follow" representedClassName=".Follow" syncable="YES" codeGenerationType="category">
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="petName" optional="YES" attributeType="String"/>
<relationship name="destination" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="followers" inverseEntity="Author"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="follows" inverseEntity="Author"/>
</entity>
<entity name="Relay" representedClassName=".Relay" syncable="YES" codeGenerationType="category">
<attribute name="address" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>
Loading

0 comments on commit 6284443

Please sign in to comment.