Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download and parse user lists #1699

Merged
merged 13 commits into from
Dec 17, 2024
Merged
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
}
}
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
Loading