diff --git a/CHANGELOG.md b/CHANGELOG.md index 63fc37963..ac06e7410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Internal Changes +- Added function for creating a new list and a test verifying list editing. [#112](https://github.com/verse-pbc/issues/issues/112) + ## [1.1] - 2025-01-03Z ### Release Notes diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj index 901d03c38..f3ea65fd5 100644 --- a/Nos.xcodeproj/project.pbxproj +++ b/Nos.xcodeproj/project.pbxproj @@ -225,6 +225,7 @@ 50E2EB7B2C8617C800D4B360 /* NSRegularExpression+Replacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */; }; 50EA86D42D28150F001E62CC /* FeedSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EA86D32D28150D001E62CC /* FeedSource.swift */; }; 50EA86D52D28150F001E62CC /* FeedSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50EA86D32D28150D001E62CC /* FeedSource.swift */; }; + 50EA885C2D2D523F001E62CC /* follow_set_with_unknown_tag.json in Resources */ = {isa = PBXBuildFile; fileRef = 50EA885B2D2D5235001E62CC /* follow_set_with_unknown_tag.json */; }; 50F695072C6392C4000E4C74 /* zap_receipt.json in Resources */ = {isa = PBXBuildFile; fileRef = 50F695062C6392C4000E4C74 /* zap_receipt.json */; }; 5B098DBC2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B098DBB2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift */; }; 5B098DC62BDAF73500500A1B /* AttributedString+Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B098DC52BDAF73500500A1B /* AttributedString+Links.swift */; }; @@ -791,6 +792,7 @@ 50DE6B1A2C6B88FE0065665D /* View+StyledBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+StyledBorder.swift"; sourceTree = ""; }; 50E2EB712C86175900D4B360 /* NSRegularExpression+Replacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+Replacement.swift"; sourceTree = ""; }; 50EA86D32D28150D001E62CC /* FeedSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedSource.swift; sourceTree = ""; }; + 50EA885B2D2D5235001E62CC /* follow_set_with_unknown_tag.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = follow_set_with_unknown_tag.json; sourceTree = ""; }; 50F695062C6392C4000E4C74 /* zap_receipt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = zap_receipt.json; sourceTree = ""; }; 5B098DBB2BDAF6CB00500A1B /* NoteParserTests+NIP08.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NoteParserTests+NIP08.swift"; sourceTree = ""; }; 5B098DC52BDAF73500500A1B /* AttributedString+Links.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Links.swift"; sourceTree = ""; }; @@ -1281,6 +1283,7 @@ 03C853C52D03A50900164D6C /* follow_set.json */, 5022F9452D2186300012FF4B /* follow_set_private.json */, 03FFCA582D075E2800D6F0F1 /* follow_set_updated.json */, + 50EA885B2D2D5235001E62CC /* follow_set_with_unknown_tag.json */, 039C96282C48321E00A8EB39 /* long_form_data.json */, C95057B02CC6986E0024EC9C /* mute_list_2.json */, C95057C42CC69A770024EC9C /* mute_list_self.json */, @@ -2303,6 +2306,7 @@ C987F83329BA951E00B44E7A /* ClarityCity-ExtraLight.otf in Resources */, C987F83D29BA951E00B44E7A /* ClarityCity-Bold.otf in Resources */, C987F84529BA951E00B44E7A /* ClarityCity-ExtraBoldItalic.otf in Resources */, + 50EA885C2D2D523F001E62CC /* follow_set_with_unknown_tag.json in Resources */, 038196F72CA36797002A94E3 /* elmo-animated.webp in Resources */, 9DB106002C650DDE00F98A30 /* Colors.xcassets in Resources */, C987F84329BA951E00B44E7A /* ClarityCity-Black.otf in Resources */, diff --git a/Nos/Models/JSONEvent+Kinds.swift b/Nos/Models/JSONEvent+Kinds.swift index 10b173f4c..54ebca4de 100644 --- a/Nos/Models/JSONEvent+Kinds.swift +++ b/Nos/Models/JSONEvent+Kinds.swift @@ -47,4 +47,39 @@ extension JSONEvent { content: reason ?? "" ) } + + /// An event that represents a list of authors. + /// - Parameters: + /// - pubKey: The public key of the user making the request. + /// - title: The title of the list. + /// - description: An optional description of the list. + /// - replaceableID: The unique identifier of the list. If left nil, one will be provided as a UUID. + /// - authorIDs: A list of author ids to add to the list. + /// - Returns: The ``JSONEvent`` representing the list. + static func followSet( + pubKey: String, + title: String, + description: String?, + replaceableID: RawReplaceableID?, + authorIDs: [RawAuthorID] + ) -> JSONEvent { + let identifier = replaceableID ?? UUID().uuidString + + var tags = [ + ["d", identifier], + ["title", title] + ] + if let description { + tags.append(["description", description]) + } + let pTags = authorIDs.map { ["p", $0] } + tags.append(contentsOf: pTags) + + return JSONEvent( + pubKey: pubKey, + kind: .followSet, + tags: tags, + content: "" + ) + } } diff --git a/Nos/Service/CurrentUser+PublishEvents.swift b/Nos/Service/CurrentUser+PublishEvents.swift index e24268695..46e193d21 100644 --- a/Nos/Service/CurrentUser+PublishEvents.swift +++ b/Nos/Service/CurrentUser+PublishEvents.swift @@ -82,6 +82,32 @@ extension CurrentUser { } } + @MainActor func publishNewList( + withTitle title: String, + description: String?, + replaceableID: RawReplaceableID? = nil, + authorIDs: [RawAuthorID] + ) async { + guard let keyPair else { + Log.debug("Error: no pubKey") + return + } + + let jsonEvent = JSONEvent.followSet( + pubKey: keyPair.publicKeyHex, + title: title, + description: description, + replaceableID: replaceableID, + authorIDs: authorIDs + ) + + do { + try await relayService.publishToAll(event: jsonEvent, signingKey: keyPair, context: viewContext) + } catch { + Log.debug("Failed to create new list \(error.localizedDescription)") + } + } + @MainActor func publishMuteList(keys: [String]) async { guard let pubKey = publicKeyHex else { Log.debug("Error: no pubKey") diff --git a/NosTests/IntegrationTests/Fixtures/follow_set_with_unknown_tag.json b/NosTests/IntegrationTests/Fixtures/follow_set_with_unknown_tag.json new file mode 100644 index 000000000..b62bba1eb --- /dev/null +++ b/NosTests/IntegrationTests/Fixtures/follow_set_with_unknown_tag.json @@ -0,0 +1,16 @@ +{ + "kind": 30000, + "id": "85e1542678164c321c413706b9c029da2355809884902dbbfd6879917148c221", + "pubkey": "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549", + "created_at": 1733516879, + "tags": [ + ["p", "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549"], + ["p", "1112cad6ffadb22c4d505e9b9f53322052e05a834822cf9368dc754cabbc7ba9"], + ["title", "A few good people"], + ["description", "They're great. Trust me."], + ["d", "listr-7ad818d7-1360-4fcb-8dbd-2ad76be88465"], + ["unknown-tag", "unknown"] + ], + "content": "", + "sig": "acdf769441a6644e3ae64f8aa1e5f4175a1045e2129e31ae806508515cb65fb5d3207a1f5f05094e09e46cb28c5a3dad5bb2886ab6f5b1faa1f4da8a9f202b04" +} diff --git a/NosTests/Models/CoreData/AuthorListTests.swift b/NosTests/Models/CoreData/AuthorListTests.swift index ff7e2393e..ae16c2523 100644 --- a/NosTests/Models/CoreData/AuthorListTests.swift +++ b/NosTests/Models/CoreData/AuthorListTests.swift @@ -65,4 +65,45 @@ final class AuthorListTests: CoreDataTestCase { // Assert XCTAssertTrue(verified) } + + @MainActor func test_update_list_pubkeys() throws { + // Arrange + let data = try jsonData(filename: "follow_set_with_unknown_tag") + let event = try JSONDecoder().decode(JSONEvent.self, from: data) + let pubkeyToRemove = "27cf2c68535ae1fc06510e827670053f5dcd39e6bd7e05f1ffb487ef2ac13549" + let pubkeyToAdd = "76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa" + + // Act + let list = try AuthorList.createOrUpdate(from: event, in: testContext) + let author = try XCTUnwrap(list.author) + + let replaceableIdentifier = try XCTUnwrap(list.replaceableIdentifier) + + // remove an author + let authorToRemove = try XCTUnwrap(list.authors.first(where: { $0.hexadecimalPublicKey == pubkeyToRemove })) + list.authors.remove(authorToRemove) + + // add an author + let rabble = try Author.findOrCreate(by: pubkeyToAdd, context: testContext) + list.addToAuthors(rabble) + + try testContext.save() + + // Assert + let request = AuthorList.event( + by: replaceableIdentifier, + author: author, + kind: EventKind.followSet.rawValue + ) + + let editedListResult = try testContext.fetch(request) + let editedList = try XCTUnwrap(editedListResult.first as? AuthorList) + + XCTAssertFalse(editedList.authors.contains(where: { $0.hexadecimalPublicKey == pubkeyToRemove })) + XCTAssertTrue(editedList.authors.contains(where: { $0.hexadecimalPublicKey == pubkeyToAdd })) + + let tags = try XCTUnwrap(editedList.allTags as? [[String]]) + XCTAssertTrue(tags.contains(where: { $0.first == "unknown-tag" })) + XCTAssertEqual(tags.count, 6) + } }