From c9e809c264d062277dd9ddad9c23f314a2f3d04a Mon Sep 17 00:00:00 2001 From: Pat Nakajima Date: Wed, 20 Sep 2023 14:19:54 -0400 Subject: [PATCH] Let each client have its own CodecRegistry (#162) * Give each client their own codec registry This should prevent crashes we've seen where many clients try to register with the global at once * Bump podspec --- Sources/XMTP/ApiClient.swift | 2 +- Sources/XMTP/Client.swift | 16 +++--- Sources/XMTP/CodecRegistry.swift | 2 +- Sources/XMTP/Codecs/AttachmentCodec.swift | 4 +- Sources/XMTP/Codecs/Composite.swift | 4 +- Sources/XMTP/Codecs/ContentCodec.swift | 12 ++--- Sources/XMTP/Codecs/DecodedComposite.swift | 4 +- Sources/XMTP/Codecs/ReactionCodec.swift | 4 +- Sources/XMTP/Codecs/ReadReceiptCodec.swift | 4 +- .../XMTP/Codecs/RemoteAttachmentCodec.swift | 8 +-- Sources/XMTP/Codecs/ReplyCodec.swift | 26 +++++----- Sources/XMTP/Codecs/TextCodec.swift | 6 +-- Sources/XMTP/ConversationV1.swift | 7 +-- Sources/XMTP/ConversationV2.swift | 8 +-- Sources/XMTP/DecodedMessage.swift | 30 +++++++---- Sources/XMTP/Messages/MessageV2.swift | 5 +- Tests/XMTPTests/AttachmentTests.swift | 4 +- Tests/XMTPTests/CodecTests.swift | 40 +++++++-------- Tests/XMTPTests/ConversationTests.swift | 8 +-- Tests/XMTPTests/ConversationsTest.swift | 2 +- Tests/XMTPTests/MessageTests.swift | 4 +- Tests/XMTPTests/ReactionTests.swift | 51 ++++++++++--------- Tests/XMTPTests/ReadReceiptTests.swift | 42 +++++++-------- Tests/XMTPTests/RemoteAttachmentTest.swift | 34 +++++++------ Tests/XMTPTests/ReplyTests.swift | 4 +- XMTP.podspec | 2 +- .../Views/ConversationListView.swift | 2 +- .../Views/MessageCellView.swift | 8 +-- .../Views/MessageListView.swift | 22 ++++---- 29 files changed, 192 insertions(+), 173 deletions(-) diff --git a/Sources/XMTP/ApiClient.swift b/Sources/XMTP/ApiClient.swift index 708a4393..3d2d335a 100644 --- a/Sources/XMTP/ApiClient.swift +++ b/Sources/XMTP/ApiClient.swift @@ -25,7 +25,7 @@ public enum ApiClientError: Error { case subscribeError(String) } -protocol ApiClient { +protocol ApiClient: Sendable { var environment: XMTPEnvironment { get } init(environment: XMTPEnvironment, secure: Bool, rustClient: XMTPRust.RustClient, appVersion: String?) throws func setAuthToken(_ token: String) diff --git a/Sources/XMTP/Client.swift b/Sources/XMTP/Client.swift index 447f2c81..2ebffcf1 100644 --- a/Sources/XMTP/Client.swift +++ b/Sources/XMTP/Client.swift @@ -50,11 +50,11 @@ public struct ClientOptions { /// 2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time). /// /// > Important: The client connects to the XMTP `dev` environment by default. Use ``ClientOptions`` to change this and other parameters of the network connection. -public class Client { +public final class Client: Sendable { /// The wallet address of the ``SigningKey`` used to create this Client. - public var address: String - var privateKeyBundleV1: PrivateKeyBundleV1 - var apiClient: ApiClient + public let address: String + let privateKeyBundleV1: PrivateKeyBundleV1 + let apiClient: ApiClient /// Access ``Conversations`` for this Client. public lazy var conversations: Conversations = .init(client: self) @@ -67,13 +67,9 @@ public class Client { apiClient.environment } - static var codecRegistry = { - var registry = CodecRegistry() - registry.register(codec: TextCodec()) - return registry - }() + var codecRegistry = CodecRegistry() - public static func register(codec: any ContentCodec) { + public func register(codec: any ContentCodec) { codecRegistry.register(codec: codec) } diff --git a/Sources/XMTP/CodecRegistry.swift b/Sources/XMTP/CodecRegistry.swift index d39f8688..d787e1f9 100644 --- a/Sources/XMTP/CodecRegistry.swift +++ b/Sources/XMTP/CodecRegistry.swift @@ -8,7 +8,7 @@ import Foundation struct CodecRegistry { - var codecs: [String: any ContentCodec] = [:] + var codecs: [String: any ContentCodec] = [TextCodec().id: TextCodec()] mutating func register(codec: any ContentCodec) { codecs[codec.id] = codec diff --git a/Sources/XMTP/Codecs/AttachmentCodec.swift b/Sources/XMTP/Codecs/AttachmentCodec.swift index 3c3ba5e8..4bc405f3 100644 --- a/Sources/XMTP/Codecs/AttachmentCodec.swift +++ b/Sources/XMTP/Codecs/AttachmentCodec.swift @@ -31,7 +31,7 @@ public struct AttachmentCodec: ContentCodec { public var contentType = ContentTypeAttachment - public func encode(content: Attachment) throws -> EncodedContent { + public func encode(content: Attachment, client _: Client) throws -> EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeAttachment @@ -44,7 +44,7 @@ public struct AttachmentCodec: ContentCodec { return encodedContent } - public func decode(content: EncodedContent) throws -> Attachment { + public func decode(content: EncodedContent, client _: Client) throws -> Attachment { guard let mimeType = content.parameters["mimeType"], let filename = content.parameters["filename"] else { diff --git a/Sources/XMTP/Codecs/Composite.swift b/Sources/XMTP/Codecs/Composite.swift index ae5374f2..f10fec0e 100644 --- a/Sources/XMTP/Codecs/Composite.swift +++ b/Sources/XMTP/Codecs/Composite.swift @@ -28,7 +28,7 @@ struct CompositeCodec: ContentCodec { ContentTypeComposite } - public func encode(content: DecodedComposite) throws -> EncodedContent { + public func encode(content: DecodedComposite, client _: Client) throws -> EncodedContent { let composite = toComposite(content: content) var encoded = EncodedContent() encoded.type = ContentTypeComposite @@ -36,7 +36,7 @@ struct CompositeCodec: ContentCodec { return encoded } - public func decode(content encoded: EncodedContent) throws -> DecodedComposite { + public func decode(content encoded: EncodedContent, client _: Client) throws -> DecodedComposite { let composite = try Composite(serializedData: encoded.content) let decodedComposite = fromComposite(composite: composite) return decodedComposite diff --git a/Sources/XMTP/Codecs/ContentCodec.swift b/Sources/XMTP/Codecs/ContentCodec.swift index c7ae44d7..45e3f75c 100644 --- a/Sources/XMTP/Codecs/ContentCodec.swift +++ b/Sources/XMTP/Codecs/ContentCodec.swift @@ -14,8 +14,8 @@ enum CodecError: String, Error { public typealias EncodedContent = Xmtp_MessageContents_EncodedContent extension EncodedContent { - public func decoded() throws -> T { - let codec = Client.codecRegistry.find(for: type) + public func decoded(with client: Client) throws -> T { + let codec = client.codecRegistry.find(for: type) var encodedContent = self @@ -23,7 +23,7 @@ extension EncodedContent { encodedContent = try decompressContent() } - if let content = try codec.decode(content: encodedContent) as? T { + if let content = try codec.decode(content: encodedContent, client: client) as? T { return content } @@ -69,9 +69,9 @@ public protocol ContentCodec: Hashable, Equatable { associatedtype T var contentType: ContentTypeID { get } - func encode(content: T) throws -> EncodedContent - func decode(content: EncodedContent) throws -> T - func fallback(content: T) throws -> String? + func encode(content: T, client: Client) throws -> EncodedContent + func decode(content: EncodedContent, client: Client) throws -> T + func fallback(content: T) throws -> String? } public extension ContentCodec { diff --git a/Sources/XMTP/Codecs/DecodedComposite.swift b/Sources/XMTP/Codecs/DecodedComposite.swift index cfd8b7f3..0719ddea 100644 --- a/Sources/XMTP/Codecs/DecodedComposite.swift +++ b/Sources/XMTP/Codecs/DecodedComposite.swift @@ -16,7 +16,7 @@ public struct DecodedComposite { self.encodedContent = encodedContent } - func content() throws -> T? { - return try encodedContent?.decoded() + func content(with client: Client) throws -> T? { + return try encodedContent?.decoded(with: client) } } diff --git a/Sources/XMTP/Codecs/ReactionCodec.swift b/Sources/XMTP/Codecs/ReactionCodec.swift index e4af6ceb..51b9ec0d 100644 --- a/Sources/XMTP/Codecs/ReactionCodec.swift +++ b/Sources/XMTP/Codecs/ReactionCodec.swift @@ -62,7 +62,7 @@ public struct ReactionCodec: ContentCodec { public init() {} - public func encode(content: Reaction) throws -> EncodedContent { + public func encode(content: Reaction, client _: Client) throws -> EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeReaction @@ -71,7 +71,7 @@ public struct ReactionCodec: ContentCodec { return encodedContent } - public func decode(content: EncodedContent) throws -> Reaction { + public func decode(content: EncodedContent, client _: Client) throws -> Reaction { // First try to decode it in the canonical form. // swiftlint:disable no_optional_try if let reaction = try? JSONDecoder().decode(Reaction.self, from: content.content) { diff --git a/Sources/XMTP/Codecs/ReadReceiptCodec.swift b/Sources/XMTP/Codecs/ReadReceiptCodec.swift index 354e8b68..6f4f35c3 100644 --- a/Sources/XMTP/Codecs/ReadReceiptCodec.swift +++ b/Sources/XMTP/Codecs/ReadReceiptCodec.swift @@ -20,7 +20,7 @@ public struct ReadReceiptCodec: ContentCodec { public var contentType = ContentTypeReadReceipt - public func encode(content: ReadReceipt) throws -> EncodedContent { + public func encode(content: ReadReceipt, client _: Client) throws -> EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeReadReceipt @@ -29,7 +29,7 @@ public struct ReadReceiptCodec: ContentCodec { return encodedContent } - public func decode(content: EncodedContent) throws -> ReadReceipt { + public func decode(content: EncodedContent, client _: Client) throws -> ReadReceipt { return ReadReceipt() } diff --git a/Sources/XMTP/Codecs/RemoteAttachmentCodec.swift b/Sources/XMTP/Codecs/RemoteAttachmentCodec.swift index bc41c14c..c50e3891 100644 --- a/Sources/XMTP/Codecs/RemoteAttachmentCodec.swift +++ b/Sources/XMTP/Codecs/RemoteAttachmentCodec.swift @@ -78,9 +78,9 @@ public struct RemoteAttachment { } } - public static func encodeEncrypted(content: T, codec: Codec) throws -> EncryptedEncodedContent where Codec.T == T { + public static func encodeEncrypted(content: T, codec: Codec, with client: Client) throws -> EncryptedEncodedContent where Codec.T == T { let secret = try Crypto.secureRandomBytes(count: 32) - let encodedContent = try codec.encode(content: content).serializedData() + let encodedContent = try codec.encode(content: content, client: client).serializedData() let ciphertext = try Crypto.encrypt(secret, encodedContent) let contentDigest = sha256(data: ciphertext.aes256GcmHkdfSha256.payload) @@ -139,7 +139,7 @@ public struct RemoteAttachmentCodec: ContentCodec { public var contentType = ContentTypeRemoteAttachment - public func encode(content: RemoteAttachment) throws -> EncodedContent { + public func encode(content: RemoteAttachment, client _: Client) throws -> EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeRemoteAttachment @@ -157,7 +157,7 @@ public struct RemoteAttachmentCodec: ContentCodec { return encodedContent } - public func decode(content: EncodedContent) throws -> RemoteAttachment { + public func decode(content: EncodedContent, client _: Client) throws -> RemoteAttachment { guard let url = String(data: content.content, encoding: .utf8) else { throw RemoteAttachmentError.invalidURL } diff --git a/Sources/XMTP/Codecs/ReplyCodec.swift b/Sources/XMTP/Codecs/ReplyCodec.swift index 754d3456..4c7c98fc 100644 --- a/Sources/XMTP/Codecs/ReplyCodec.swift +++ b/Sources/XMTP/Codecs/ReplyCodec.swift @@ -14,11 +14,11 @@ public struct Reply { public var content: Any public var contentType: ContentTypeID - public init(reference: String, content: Any, contentType: ContentTypeID) { - self.reference = reference - self.content = content - self.contentType = contentType - } + public init(reference: String, content: Any, contentType: ContentTypeID) { + self.reference = reference + self.content = content + self.contentType = contentType + } } public struct ReplyCodec: ContentCodec { @@ -26,27 +26,27 @@ public struct ReplyCodec: ContentCodec { public init() {} - public func encode(content reply: Reply) throws -> EncodedContent { + public func encode(content reply: Reply, client: Client) throws -> EncodedContent { var encodedContent = EncodedContent() - let replyCodec = Client.codecRegistry.find(for: reply.contentType) + let replyCodec = client.codecRegistry.find(for: reply.contentType) encodedContent.type = contentType // TODO: cut when we're certain no one is looking for "contentType" here. encodedContent.parameters["contentType"] = reply.contentType.description encodedContent.parameters["reference"] = reply.reference - encodedContent.content = try encodeReply(codec: replyCodec, content: reply.content).serializedData() + encodedContent.content = try encodeReply(codec: replyCodec, content: reply.content, client: client).serializedData() return encodedContent } - public func decode(content: EncodedContent) throws -> Reply { + public func decode(content: EncodedContent, client: Client) throws -> Reply { guard let reference = content.parameters["reference"] else { throw CodecError.invalidContent } let replyEncodedContent = try EncodedContent(serializedData: content.content) - let replyCodec = Client.codecRegistry.find(for: replyEncodedContent.type) - let replyContent = try replyCodec.decode(content: replyEncodedContent) + let replyCodec = client.codecRegistry.find(for: replyEncodedContent.type) + let replyContent = try replyCodec.decode(content: replyEncodedContent, client: client) return Reply( reference: reference, @@ -55,9 +55,9 @@ public struct ReplyCodec: ContentCodec { ) } - func encodeReply(codec: Codec, content: Any) throws -> EncodedContent { + func encodeReply(codec: Codec, content: Any, client: Client) throws -> EncodedContent { if let content = content as? Codec.T { - return try codec.encode(content: content) + return try codec.encode(content: content, client: client) } else { throw CodecError.invalidContent } diff --git a/Sources/XMTP/Codecs/TextCodec.swift b/Sources/XMTP/Codecs/TextCodec.swift index 54e29018..64b6db07 100644 --- a/Sources/XMTP/Codecs/TextCodec.swift +++ b/Sources/XMTP/Codecs/TextCodec.swift @@ -17,11 +17,11 @@ public struct TextCodec: ContentCodec { public typealias T = String - public init() {} + public init() { } public var contentType = ContentTypeText - public func encode(content: String) throws -> EncodedContent { + public func encode(content: String, client _: Client) throws -> EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeText @@ -31,7 +31,7 @@ public struct TextCodec: ContentCodec { return encodedContent } - public func decode(content: EncodedContent) throws -> String { + public func decode(content: EncodedContent, client _: Client) throws -> String { if let encoding = content.parameters["encoding"], encoding != "UTF-8" { throw TextCodecError.invalidEncoding } diff --git a/Sources/XMTP/ConversationV1.swift b/Sources/XMTP/ConversationV1.swift index 8bb922d2..cd503226 100644 --- a/Sources/XMTP/ConversationV1.swift +++ b/Sources/XMTP/ConversationV1.swift @@ -92,11 +92,11 @@ public struct ConversationV1 { } func prepareMessage(content: T, options: SendOptions?) async throws -> PreparedMessage { - let codec = Client.codecRegistry.find(for: options?.contentType) + let codec = client.codecRegistry.find(for: options?.contentType) func encode(codec: Codec, content: Any) throws -> EncodedContent { if let content = content as? Codec.T { - return try codec.encode(content: content) + return try codec.encode(content: content, client: client) } else { throw CodecError.invalidContent } @@ -203,7 +203,8 @@ public struct ConversationV1 { let header = try message.v1.header var decoded = DecodedMessage( - topic: envelope.contentTopic, + client: client, + topic: envelope.contentTopic, encodedContent: encodedMessage, senderAddress: header.sender.walletAddress, sent: message.v1.sentAt diff --git a/Sources/XMTP/ConversationV2.swift b/Sources/XMTP/ConversationV2.swift index 2d20bb7d..263897d1 100644 --- a/Sources/XMTP/ConversationV2.swift +++ b/Sources/XMTP/ConversationV2.swift @@ -87,11 +87,11 @@ public struct ConversationV2 { } func prepareMessage(content: T, options: SendOptions?) async throws -> PreparedMessage { - let codec = Client.codecRegistry.find(for: options?.contentType) + let codec = client.codecRegistry.find(for: options?.contentType) func encode(codec: Codec, content: Any) throws -> EncodedContent { if let content = content as? Codec.T { - return try codec.encode(content: content) + return try codec.encode(content: content, client: client) } else { throw CodecError.invalidContent } @@ -176,7 +176,7 @@ public struct ConversationV2 { } private func decode(_ message: MessageV2) throws -> DecodedMessage { - try MessageV2.decode(message, keyMaterial: keyMaterial) + try MessageV2.decode(message, keyMaterial: keyMaterial, client: client) } @discardableResult func send(content: T, options: SendOptions? = nil) async throws -> String { @@ -200,7 +200,7 @@ public struct ConversationV2 { } public func encode(codec: Codec, content: T) async throws -> Data where Codec.T == T { - let content = try codec.encode(content: content) + let content = try codec.encode(content: content, client: client) let message = try await MessageV2.encode( client: client, diff --git a/Sources/XMTP/DecodedMessage.swift b/Sources/XMTP/DecodedMessage.swift index fa31a081..ac6f06d1 100644 --- a/Sources/XMTP/DecodedMessage.swift +++ b/Sources/XMTP/DecodedMessage.swift @@ -9,7 +9,7 @@ import Foundation /// Decrypted messages from a conversation. public struct DecodedMessage: Sendable { - public var topic: String + public var topic: String public var id: String = "" @@ -21,15 +21,24 @@ public struct DecodedMessage: Sendable { /// When the message was sent public var sent: Date - public init(topic: String, encodedContent: EncodedContent, senderAddress: String, sent: Date) { - self.topic = topic - self.encodedContent = encodedContent - self.senderAddress = senderAddress - self.sent = sent + var client: Client + + public init( + client: Client, + topic: String, + encodedContent: EncodedContent, + senderAddress: String, + sent: Date + ) { + self.client = client + self.topic = topic + self.encodedContent = encodedContent + self.senderAddress = senderAddress + self.sent = sent } public func content() throws -> T { - return try encodedContent.decoded() + return try encodedContent.decoded(with: client) } public var fallbackContent: String { @@ -46,10 +55,11 @@ public struct DecodedMessage: Sendable { } public extension DecodedMessage { - static func preview(topic: String, body: String, senderAddress: String, sent: Date) -> DecodedMessage { + static func preview(client: Client, topic: String, body: String, senderAddress: String, sent: Date) -> DecodedMessage { // swiftlint:disable force_try - let encoded = try! TextCodec().encode(content: body) + let encoded = try! TextCodec().encode(content: body, client: client) // swiftlint:enable force_try - return DecodedMessage(topic: topic, encodedContent: encoded, senderAddress: senderAddress, sent: sent) + + return DecodedMessage(client: client, topic: topic, encodedContent: encoded, senderAddress: senderAddress, sent: sent) } } diff --git a/Sources/XMTP/Messages/MessageV2.swift b/Sources/XMTP/Messages/MessageV2.swift index 973a52fc..0b65ffbf 100644 --- a/Sources/XMTP/Messages/MessageV2.swift +++ b/Sources/XMTP/Messages/MessageV2.swift @@ -22,7 +22,7 @@ extension MessageV2 { self.ciphertext = ciphertext } - static func decode(_ message: MessageV2, keyMaterial: Data) throws -> DecodedMessage { + static func decode(_ message: MessageV2, keyMaterial: Data, client: Client) throws -> DecodedMessage { do { let decrypted = try Crypto.decrypt(keyMaterial, message.ciphertext, additionalData: message.headerBytes) let signed = try SignedContent(serializedData: decrypted) @@ -53,7 +53,8 @@ extension MessageV2 { let header = try MessageHeaderV2(serializedData: message.headerBytes) return DecodedMessage( - topic: header.topic, + client: client, + topic: header.topic, encodedContent: encodedMessage, senderAddress: try signed.sender.walletAddress, sent: Date(timeIntervalSince1970: Double(header.createdNs / 1_000_000) / 1000) diff --git a/Tests/XMTPTests/AttachmentTests.swift b/Tests/XMTPTests/AttachmentTests.swift index 7b5b1783..6a0d6d48 100644 --- a/Tests/XMTPTests/AttachmentTests.swift +++ b/Tests/XMTPTests/AttachmentTests.swift @@ -12,13 +12,13 @@ import XCTest @available(iOS 15, *) class AttachmentsTests: XCTestCase { func testCanUseAttachmentCodec() async throws { - Client.register(codec: AttachmentCodec()) - // swiftlint:disable force_try let iconData = Data(base64Encoded: Data("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".utf8))! let fixtures = await fixtures() let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + fixtures.aliceClient.register(codec: AttachmentCodec()) + try await conversation.send(content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), options: .init(contentType: ContentTypeAttachment)) let messages = try await conversation.messages() diff --git a/Tests/XMTPTests/CodecTests.swift b/Tests/XMTPTests/CodecTests.swift index 2165c0b4..258a896a 100644 --- a/Tests/XMTPTests/CodecTests.swift +++ b/Tests/XMTPTests/CodecTests.swift @@ -9,9 +9,9 @@ import XCTest @testable import XMTP struct NumberCodec: ContentCodec { - func fallback(content: Double) throws -> String? { - return "pi" - } + func fallback(content: Double) throws -> String? { + return "pi" + } typealias T = Double @@ -19,7 +19,7 @@ struct NumberCodec: ContentCodec { ContentTypeID(authorityID: "example.com", typeID: "number", versionMajor: 1, versionMinor: 1) } - func encode(content: Double) throws -> XMTP.EncodedContent { + func encode(content: Double, client _: Client) throws -> XMTP.EncodedContent { var encodedContent = EncodedContent() encodedContent.type = ContentTypeID(authorityID: "example.com", typeID: "number", versionMajor: 1, versionMinor: 1) @@ -28,7 +28,7 @@ struct NumberCodec: ContentCodec { return encodedContent } - func decode(content: XMTP.EncodedContent) throws -> Double { + func decode(content: XMTP.EncodedContent, client _: Client) throws -> Double { return try JSONDecoder().decode(Double.self, from: content.content) } } @@ -36,13 +36,13 @@ struct NumberCodec: ContentCodec { @available(iOS 15, *) class CodecTests: XCTestCase { func testCanRoundTripWithCustomContentType() async throws { - Client.register(codec: NumberCodec()) - let fixtures = await fixtures() let aliceClient = fixtures.aliceClient! let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) + aliceClient.register(codec: NumberCodec()) + try await aliceConversation.send(content: 3.14, options: .init(contentType: NumberCodec().contentType)) let messages = try await aliceConversation.messages() @@ -55,16 +55,17 @@ class CodecTests: XCTestCase { } func testFallsBackToFallbackContentWhenCannotDecode() async throws { - Client.register(codec: NumberCodec()) let fixtures = await fixtures() let aliceClient = fixtures.aliceClient! let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) + aliceClient.register(codec: NumberCodec()) + try await aliceConversation.send(content: 3.14, options: .init(contentType: NumberCodec().contentType)) // Remove number codec from registry - Client.codecRegistry.codecs.removeValue(forKey: NumberCodec().id) + aliceClient.codecRegistry.codecs.removeValue(forKey: NumberCodec().id) let messages = try await aliceConversation.messages() XCTAssertEqual(messages.count, 1) @@ -75,33 +76,32 @@ class CodecTests: XCTestCase { } func testCompositeCodecOnePart() async throws { - Client.register(codec: CompositeCodec()) - let fixtures = await fixtures() let aliceClient = fixtures.aliceClient! + aliceClient.register(codec: CompositeCodec()) let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) - let textContent = try TextCodec().encode(content: "hiya") + let textContent = try TextCodec().encode(content: "hiya", client: aliceClient) let source = DecodedComposite(encodedContent: textContent) try await aliceConversation.send(content: source, options: .init(contentType: CompositeCodec().contentType)) let messages = try await aliceConversation.messages() let decoded: DecodedComposite = try messages[0].content() - XCTAssertEqual("hiya", try decoded.content()) + XCTAssertEqual("hiya", try decoded.content(with: aliceClient)) } func testCompositeCodecCanHaveParts() async throws { - Client.register(codec: CompositeCodec()) - Client.register(codec: NumberCodec()) - let fixtures = await fixtures() let aliceClient = fixtures.aliceClient! let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) - let textContent = try TextCodec().encode(content: "sup") - let numberContent = try NumberCodec().encode(content: 3.14) + aliceClient.register(codec: CompositeCodec()) + aliceClient.register(codec: NumberCodec()) + + let textContent = try TextCodec().encode(content: "sup", client: aliceClient) + let numberContent = try NumberCodec().encode(content: 3.14, client: aliceClient) let source = DecodedComposite(parts: [ DecodedComposite(encodedContent: textContent), @@ -116,7 +116,7 @@ class CodecTests: XCTestCase { let decoded: DecodedComposite = try messages[0].content() let part1 = decoded.parts[0] let part2 = decoded.parts[1].parts[0] - XCTAssertEqual("sup", try part1.content()) - XCTAssertEqual(3.14, try part2.content()) + XCTAssertEqual("sup", try part1.content(with: aliceClient)) + XCTAssertEqual(3.14, try part2.content(with: aliceClient)) } } diff --git a/Tests/XMTPTests/ConversationTests.swift b/Tests/XMTPTests/ConversationTests.swift index b1d824cd..b6ede00a 100644 --- a/Tests/XMTPTests/ConversationTests.swift +++ b/Tests/XMTPTests/ConversationTests.swift @@ -218,7 +218,7 @@ class ConversationTests: XCTestCase { } let encoder = TextCodec() - let encodedContent = try encoder.encode(content: "hi alice") + let encodedContent = try encoder.encode(content: "hi alice", client: aliceClient) // Stream a message fakeApiClient.send( @@ -265,8 +265,8 @@ class ConversationTests: XCTestCase { } let codec = TextCodec() - let originalContent = try codec.encode(content: "hello") - let tamperedContent = try codec.encode(content: "this is a fake") + let originalContent = try codec.encode(content: "hello", client: aliceClient) + let tamperedContent = try codec.encode(content: "this is a fake", client: aliceClient) let originalPayload = try originalContent.serializedData() let tamperedPayload = try tamperedContent.serializedData() @@ -560,7 +560,7 @@ class ConversationTests: XCTestCase { return } - let encodedContent = try TextCodec().encode(content: "hi") + let encodedContent = try TextCodec().encode(content: "hi", client: aliceClient) try await bobConversation.send(encodedContent: encodedContent) diff --git a/Tests/XMTPTests/ConversationsTest.swift b/Tests/XMTPTests/ConversationsTest.swift index 76396403..c68810de 100644 --- a/Tests/XMTPTests/ConversationsTest.swift +++ b/Tests/XMTPTests/ConversationsTest.swift @@ -23,7 +23,7 @@ class ConversationsTests: XCTestCase { let message = try MessageV1.encode( sender: newClient.privateKeyBundleV1, recipient: fixtures.aliceClient.v1keys.toPublicKeyBundle(), - message: try TextCodec().encode(content: "hello").serializedData(), + message: try TextCodec().encode(content: "hello", client: client).serializedData(), timestamp: created ) diff --git a/Tests/XMTPTests/MessageTests.swift b/Tests/XMTPTests/MessageTests.swift index e1931553..fda95262 100644 --- a/Tests/XMTPTests/MessageTests.swift +++ b/Tests/XMTPTests/MessageTests.swift @@ -62,10 +62,10 @@ class MessageTests: XCTestCase { ) let sealedInvitation = try SealedInvitation.createV1(sender: alice.toV2(), recipient: bob.toV2().getPublicKeyBundle(), created: Date(), invitation: invitationv1) let encoder = TextCodec() - let encodedContent = try encoder.encode(content: "Yo!") + let encodedContent = try encoder.encode(content: "Yo!", client: client) let message1 = try await MessageV2.encode(client: client, content: encodedContent, topic: invitationv1.topic, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial) - let decoded = try MessageV2.decode(message1, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial) + let decoded = try MessageV2.decode(message1, keyMaterial: invitationv1.aes256GcmHkdfSha256.keyMaterial, client: client) let result: String = try decoded.content() XCTAssertEqual(result, "Yo!") } diff --git a/Tests/XMTPTests/ReactionTests.swift b/Tests/XMTPTests/ReactionTests.swift index 4cbc7212..797a1dcc 100644 --- a/Tests/XMTPTests/ReactionTests.swift +++ b/Tests/XMTPTests/ReactionTests.swift @@ -41,25 +41,26 @@ class ReactionTests: XCTestCase { $0.content = Data("smile".utf8) } - let canonical = try codec.decode(content: canonicalEncoded) - let legacy = try codec.decode(content: legacyEncoded) - - XCTAssertEqual(ReactionAction.added, canonical.action) - XCTAssertEqual(ReactionAction.added, legacy.action) - XCTAssertEqual("smile", canonical.content) - XCTAssertEqual("smile", legacy.content) - XCTAssertEqual("abc123", canonical.reference) - XCTAssertEqual("abc123", legacy.reference) - XCTAssertEqual(ReactionSchema.shortcode, canonical.schema) - XCTAssertEqual(ReactionSchema.shortcode, legacy.schema) + let fixtures = await fixtures() + let canonical = try codec.decode(content: canonicalEncoded, client: fixtures.aliceClient) + let legacy = try codec.decode(content: legacyEncoded, client: fixtures.aliceClient) + + XCTAssertEqual(ReactionAction.added, canonical.action) + XCTAssertEqual(ReactionAction.added, legacy.action) + XCTAssertEqual("smile", canonical.content) + XCTAssertEqual("smile", legacy.content) + XCTAssertEqual("abc123", canonical.reference) + XCTAssertEqual("abc123", legacy.reference) + XCTAssertEqual(ReactionSchema.shortcode, canonical.schema) + XCTAssertEqual(ReactionSchema.shortcode, legacy.schema) } func testCanUseReactionCodec() async throws { - Client.register(codec: ReactionCodec()) - let fixtures = await fixtures() let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + fixtures.aliceClient.register(codec: ReactionCodec()) + try await conversation.send(text: "hey alice 2 bob") let messageToReact = try await conversation.messages()[0] @@ -114,16 +115,18 @@ class ReactionTests: XCTestCase { $0.content = Data("smile".utf8) } - let canonical = try codec.decode(content: canonicalEncoded) - let legacy = try codec.decode(content: legacyEncoded) - - XCTAssertEqual(ReactionAction.unknown, canonical.action) - XCTAssertEqual(ReactionAction.unknown, legacy.action) - XCTAssertEqual("smile", canonical.content) - XCTAssertEqual("smile", legacy.content) - XCTAssertEqual("", canonical.reference) - XCTAssertEqual("", legacy.reference) - XCTAssertEqual(ReactionSchema.unknown, canonical.schema) - XCTAssertEqual(ReactionSchema.unknown, legacy.schema) + let fixtures = await fixtures() + + let canonical = try codec.decode(content: canonicalEncoded, client: fixtures.aliceClient) + let legacy = try codec.decode(content: legacyEncoded, client: fixtures.aliceClient) + + XCTAssertEqual(ReactionAction.unknown, canonical.action) + XCTAssertEqual(ReactionAction.unknown, legacy.action) + XCTAssertEqual("smile", canonical.content) + XCTAssertEqual("smile", legacy.content) + XCTAssertEqual("", canonical.reference) + XCTAssertEqual("", legacy.reference) + XCTAssertEqual(ReactionSchema.unknown, canonical.schema) + XCTAssertEqual(ReactionSchema.unknown, legacy.schema) } } diff --git a/Tests/XMTPTests/ReadReceiptTests.swift b/Tests/XMTPTests/ReadReceiptTests.swift index 15191cc8..8a91e38b 100644 --- a/Tests/XMTPTests/ReadReceiptTests.swift +++ b/Tests/XMTPTests/ReadReceiptTests.swift @@ -12,25 +12,25 @@ import XCTest @available(iOS 15, *) class ReadReceiptTests: XCTestCase { - func testCanUseReadReceiptCodec() async throws { - Client.register(codec: ReadReceiptCodec()) - - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) - - try await conversation.send(text: "hey alice 2 bob") - - let read = ReadReceipt() - - try await conversation.send( - content: read, - options: .init(contentType: ContentTypeReadReceipt) - ) - - let updatedMessages = try await conversation.messages() - - let message = try await conversation.messages()[0] - let contentType: String = message.encodedContent.type.typeID - XCTAssertEqual("readReceipt", contentType) - } + func testCanUseReadReceiptCodec() async throws { + let fixtures = await fixtures() + fixtures.aliceClient.register(codec: ReadReceiptCodec()) + + let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + + try await conversation.send(text: "hey alice 2 bob") + + let read = ReadReceipt() + + try await conversation.send( + content: read, + options: .init(contentType: ContentTypeReadReceipt) + ) + + let updatedMessages = try await conversation.messages() + + let message = try await conversation.messages()[0] + let contentType: String = message.encodedContent.type.typeID + XCTAssertEqual("readReceipt", contentType) + } } diff --git a/Tests/XMTPTests/RemoteAttachmentTest.swift b/Tests/XMTPTests/RemoteAttachmentTest.swift index 977202be..1af23159 100644 --- a/Tests/XMTPTests/RemoteAttachmentTest.swift +++ b/Tests/XMTPTests/RemoteAttachmentTest.swift @@ -31,12 +31,13 @@ class RemoteAttachmentTests: XCTestCase { } func testBasic() async throws { - Client.register(codec: AttachmentCodec()) - Client.register(codec: RemoteAttachmentCodec()) - let fixtures = await fixtures() + + fixtures.aliceClient.register(codec: AttachmentCodec()) + fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) + let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) - let enecryptedEncodedContent = try RemoteAttachment.encodeEncrypted(content: "Hello", codec: TextCodec()) + let enecryptedEncodedContent = try RemoteAttachment.encodeEncrypted(content: "Hello", codec: TextCodec(), with: fixtures.aliceClient) var remoteAttachmentContent = try RemoteAttachment(url: "https://example.com", encryptedEncodedContent: enecryptedEncodedContent) remoteAttachmentContent.filename = "hello.txt" remoteAttachmentContent.contentLength = 5 @@ -45,18 +46,19 @@ class RemoteAttachmentTests: XCTestCase { } func testCanUseAttachmentCodec() async throws { - Client.register(codec: AttachmentCodec()) - Client.register(codec: RemoteAttachmentCodec()) - let fixtures = await fixtures() guard case let .v2(conversation) = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) else { XCTFail("no v2 convo") return } + fixtures.aliceClient.register(codec: AttachmentCodec()) + fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) + let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), - codec: AttachmentCodec() + codec: AttachmentCodec(), + with: fixtures.aliceClient ) let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) @@ -84,7 +86,7 @@ class RemoteAttachmentTests: XCTestCase { remoteAttachment.fetcher = TestFetcher() let encodedContent: EncodedContent = try await remoteAttachment.content() - let attachment: Attachment = try encodedContent.decoded() + let attachment: Attachment = try encodedContent.decoded(with: fixtures.aliceClient) XCTAssertEqual("icon.png", attachment.filename) XCTAssertEqual("image/png", attachment.mimeType) @@ -93,18 +95,19 @@ class RemoteAttachmentTests: XCTestCase { } func testCannotUseNonHTTPSUrl() async throws { - Client.register(codec: AttachmentCodec()) - Client.register(codec: RemoteAttachmentCodec()) - let fixtures = await fixtures() guard case let .v2(conversation) = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) else { XCTFail("no v2 convo") return } + fixtures.aliceClient.register(codec: AttachmentCodec()) + fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) + let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), - codec: AttachmentCodec() + codec: AttachmentCodec(), + with: fixtures.aliceClient ) let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) @@ -122,14 +125,15 @@ class RemoteAttachmentTests: XCTestCase { func testVerifiesContentDigest() async throws { let fixtures = await fixtures() - guard case let .v2(conversation) = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) else { + guard case let .v2(_) = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) else { XCTFail("no v2 convo") return } let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), - codec: AttachmentCodec() + codec: AttachmentCodec(), + with: fixtures.aliceClient ) let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) diff --git a/Tests/XMTPTests/ReplyTests.swift b/Tests/XMTPTests/ReplyTests.swift index 90a35728..72a7dbae 100644 --- a/Tests/XMTPTests/ReplyTests.swift +++ b/Tests/XMTPTests/ReplyTests.swift @@ -12,11 +12,11 @@ import XCTest @available(iOS 15, *) class ReplyTests: XCTestCase { func testCanUseReplyCodec() async throws { - Client.register(codec: ReplyCodec()) - let fixtures = await fixtures() let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + fixtures.aliceClient.register(codec: ReplyCodec()) + try await conversation.send(text: "hey alice 2 bob") let messageToReply = try await conversation.messages()[0] diff --git a/XMTP.podspec b/XMTP.podspec index 75c969ae..30c2d46e 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.5.8-alpha0" + spec.version = "0.5.9-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. diff --git a/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift b/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift index 14a6d386..f92c2465 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift @@ -35,7 +35,7 @@ struct ConversationListView: View { } .task { do { - for try await conversation in client.conversations.stream() { + for try await conversation in await client.conversations.stream() { conversations.insert(conversation, at: 0) await add(conversations: [conversation]) diff --git a/XMTPiOSExample/XMTPiOSExample/Views/MessageCellView.swift b/XMTPiOSExample/XMTPiOSExample/Views/MessageCellView.swift index 040eb0fe..624c015e 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/MessageCellView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/MessageCellView.swift @@ -71,9 +71,11 @@ struct MessageCellView: View { struct MessageCellView_Previews: PreviewProvider { static var previews: some View { - List { - MessageCellView(myAddress: "0x00", message: DecodedMessage.preview(body: "Hi, how is it going?", senderAddress: "0x00", sent: Date())) + PreviewClientProvider { client in + List { + MessageCellView(myAddress: "0x00", message: DecodedMessage.preview(client: client, topic: "foo", body: "Hi, how is it going?", senderAddress: "0x00", sent: Date())) + } + .listStyle(.plain) } - .listStyle(.plain) } } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/MessageListView.swift b/XMTPiOSExample/XMTPiOSExample/Views/MessageListView.swift index 7b2ec178..9b3da2b2 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/MessageListView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/MessageListView.swift @@ -36,15 +36,17 @@ struct MessageListView: View { struct MessageListView_Previews: PreviewProvider { static var previews: some View { - MessageListView( - myAddress: "0x00", messages: [ - XMTP.DecodedMessage.preview(body: "Hello", senderAddress: "0x00", sent: Date().addingTimeInterval(-10)), - XMTP.DecodedMessage.preview(body: "Oh hi", senderAddress: "0x01", sent: Date().addingTimeInterval(-9)), - XMTP.DecodedMessage.preview(body: "Sup", senderAddress: "0x01", sent: Date().addingTimeInterval(-8)), - XMTP.DecodedMessage.preview(body: "Nice to see you", senderAddress: "0x00", sent: Date().addingTimeInterval(-7)), - XMTP.DecodedMessage.preview(body: "What if it's a longer message I mean really really long like should it wrap?", senderAddress: "0x01", sent: Date().addingTimeInterval(-6)), - XMTP.DecodedMessage.preview(body: "🧐", senderAddress: "0x00", sent: Date().addingTimeInterval(-5)), - ] - ) + PreviewClientProvider { client in + MessageListView( + myAddress: "0x00", messages: [ + XMTP.DecodedMessage.preview(client: client, topic: "foo", body: "Hello", senderAddress: "0x00", sent: Date().addingTimeInterval(-10)), + XMTP.DecodedMessage.preview(client: client, topic: "foo",body: "Oh hi", senderAddress: "0x01", sent: Date().addingTimeInterval(-9)), + XMTP.DecodedMessage.preview(client: client, topic: "foo",body: "Sup", senderAddress: "0x01", sent: Date().addingTimeInterval(-8)), + XMTP.DecodedMessage.preview(client: client, topic: "foo",body: "Nice to see you", senderAddress: "0x00", sent: Date().addingTimeInterval(-7)), + XMTP.DecodedMessage.preview(client: client, topic: "foo",body: "What if it's a longer message I mean really really long like should it wrap?", senderAddress: "0x01", sent: Date().addingTimeInterval(-6)), + XMTP.DecodedMessage.preview(client: client, topic: "foo",body: "🧐", senderAddress: "0x00", sent: Date().addingTimeInterval(-5)), + ] + ) + } } }