Compare commits

...

2 Commits

Author SHA1 Message Date
Pat Nakajima 56c4f9f5d0
Add group chat features (#109)
* sendable

* resolved

* Add some group chat codecs

* Add isGroup to Conversation
2023-06-13 13:45:12 -07:00
Daniel McCartney b5db26c75d
fix: use the proper production URL (#107) 2023-06-06 12:41:39 -07:00
14 changed files with 155 additions and 21 deletions

View File

@ -70,7 +70,7 @@ class GRPCApiClient: ApiClient {
switch env { switch env {
case XMTPEnvironment.local: return "http://localhost:5556" case XMTPEnvironment.local: return "http://localhost:5556"
case XMTPEnvironment.dev: return "https://dev.xmtp.network:5556" case XMTPEnvironment.dev: return "https://dev.xmtp.network:5556"
case XMTPEnvironment.production: return "https://xmtp.network:5556" case XMTPEnvironment.production: return "https://production.xmtp.network:5556"
} }
} }

View File

@ -48,6 +48,8 @@ public class Client {
var privateKeyBundleV1: PrivateKeyBundleV1 var privateKeyBundleV1: PrivateKeyBundleV1
var apiClient: ApiClient var apiClient: ApiClient
public private(set) var isGroupChatEnabled = false
/// Access ``Conversations`` for this Client. /// Access ``Conversations`` for this Client.
public lazy var conversations: Conversations = .init(client: self) public lazy var conversations: Conversations = .init(client: self)
@ -167,6 +169,11 @@ public class Client {
self.apiClient = apiClient self.apiClient = apiClient
} }
public func enableGroupChat() {
self.isGroupChatEnabled = true
GroupChat.registerCodecs()
}
public var privateKeyBundle: PrivateKeyBundle { public var privateKeyBundle: PrivateKeyBundle {
PrivateKeyBundle(v1: privateKeyBundleV1) PrivateKeyBundle(v1: privateKeyBundleV1)
} }

View File

@ -0,0 +1,33 @@
//
// GroupChatMemberAddedCodec.swift
//
//
// Created by Pat Nakajima on 6/11/23.
//
import Foundation
public let ContentTypeGroupChatMemberAdded = ContentTypeID(authorityID: "xmtp.org", typeID: "groupChatMemberAdded", versionMajor: 1, versionMinor: 0)
public struct GroupChatMemberAdded: Codable {
// The address of the member being added
public var member: String
}
public struct GroupChatMemberAddedCodec: ContentCodec {
public var contentType = ContentTypeGroupChatMemberAdded
public func encode(content: GroupChatMemberAdded) throws -> EncodedContent {
var encodedContent = EncodedContent()
encodedContent.type = ContentTypeGroupChatMemberAdded
encodedContent.content = try JSONEncoder().encode(content)
return encodedContent
}
public func decode(content: EncodedContent) throws -> GroupChatMemberAdded {
let memberAdded = try JSONDecoder().decode(GroupChatMemberAdded.self, from: content.content)
return memberAdded
}
}

View File

@ -0,0 +1,33 @@
//
// GroupChatTitleChangedCodec.swift
//
//
// Created by Pat Nakajima on 6/11/23.
//
import Foundation
public let ContentTypeGroupTitleChangedAdded = ContentTypeID(authorityID: "xmtp.org", typeID: "groupChatTitleChanged", versionMajor: 1, versionMinor: 0)
public struct GroupChatTitleChanged: Codable {
// The new title
public var newTitle: String
}
public struct GroupChatTitleChangedCodec: ContentCodec {
public var contentType = ContentTypeGroupTitleChangedAdded
public func encode(content: GroupChatTitleChanged) throws -> EncodedContent {
var encodedContent = EncodedContent()
encodedContent.type = ContentTypeGroupTitleChangedAdded
encodedContent.content = try JSONEncoder().encode(content)
return encodedContent
}
public func decode(content: EncodedContent) throws -> GroupChatTitleChanged {
let titleChanged = try JSONDecoder().decode(GroupChatTitleChanged.self, from: content.content)
return titleChanged
}
}

View File

@ -21,7 +21,7 @@ public enum ConversationContainer: Codable {
} }
/// Wrapper that provides a common interface between ``ConversationV1`` and ``ConversationV2`` objects. /// Wrapper that provides a common interface between ``ConversationV1`` and ``ConversationV2`` objects.
public enum Conversation { public enum Conversation: Sendable {
// TODO: It'd be nice to not have to expose these types as public, maybe we make this a struct with an enum prop instead of just an enum // TODO: It'd be nice to not have to expose these types as public, maybe we make this a struct with an enum prop instead of just an enum
case v1(ConversationV1), v2(ConversationV2) case v1(ConversationV1), v2(ConversationV2)
@ -29,11 +29,20 @@ public enum Conversation {
case v1, v2 case v1, v2
} }
public var isGroup: Bool {
switch self {
case .v1:
return false
case let .v2(conversationV2):
return conversationV2.isGroup
}
}
public var version: Version { public var version: Version {
switch self { switch self {
case let .v1: case .v1:
return .v1 return .v1
case let .v2: case .v2:
return .v2 return .v2
} }
} }

View File

@ -30,9 +30,10 @@ public struct ConversationV2 {
public var context: InvitationV1.Context public var context: InvitationV1.Context
public var peerAddress: String public var peerAddress: String
public var client: Client public var client: Client
public var isGroup = false
private var header: SealedInvitationHeaderV1 private var header: SealedInvitationHeaderV1
static func create(client: Client, invitation: InvitationV1, header: SealedInvitationHeaderV1) throws -> ConversationV2 { static func create(client: Client, invitation: InvitationV1, header: SealedInvitationHeaderV1, isGroup: Bool = false) throws -> ConversationV2 {
let myKeys = client.keys.getPublicKeyBundle() let myKeys = client.keys.getPublicKeyBundle()
let peer = try myKeys.walletAddress == (try header.sender.walletAddress) ? header.recipient : header.sender let peer = try myKeys.walletAddress == (try header.sender.walletAddress) ? header.recipient : header.sender
@ -46,7 +47,8 @@ public struct ConversationV2 {
context: invitation.context, context: invitation.context,
peerAddress: peerAddress, peerAddress: peerAddress,
client: client, client: client,
header: header header: header,
isGroup: isGroup
) )
} }
@ -59,13 +61,14 @@ public struct ConversationV2 {
header = SealedInvitationHeaderV1() header = SealedInvitationHeaderV1()
} }
public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, header: SealedInvitationHeaderV1) { public init(topic: String, keyMaterial: Data, context: InvitationV1.Context, peerAddress: String, client: Client, header: SealedInvitationHeaderV1, isGroup: Bool = false) {
self.topic = topic self.topic = topic
self.keyMaterial = keyMaterial self.keyMaterial = keyMaterial
self.context = context self.context = context
self.peerAddress = peerAddress self.peerAddress = peerAddress
self.client = client self.client = client
self.header = header self.header = header
self.isGroup = isGroup
} }
public var encodedContainer: ConversationV2Container { public var encodedContainer: ConversationV2Container {

View File

@ -227,15 +227,20 @@ public class Conversations {
print("Error loading introduction peers: \(error)") print("Error loading introduction peers: \(error)")
} }
let invitations = try await listInvitations() for sealedInvitation in try await listInvitations() {
for sealedInvitation in invitations {
do { do {
let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys)
let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)
conversations.append( conversations.append(
Conversation.v2(conversation) Conversation.v2(try conversation(from: sealedInvitation))
)
} catch {
print("Error loading invitations: \(error)")
}
}
for sealedInvitation in try await listGroupInvitations() {
do {
conversations.append(
Conversation.v2(try conversation(from: sealedInvitation, isGroup: true))
) )
} catch { } catch {
print("Error loading invitations: \(error)") print("Error loading invitations: \(error)")
@ -247,6 +252,13 @@ public class Conversations {
return self.conversations return self.conversations
} }
func conversation(from sealedInvitation: SealedInvitation, isGroup: Bool = false) throws -> ConversationV2 {
let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys)
let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header, isGroup: isGroup)
return conversation
}
func listIntroductionPeers() async throws -> [String: Date] { func listIntroductionPeers() async throws -> [String: Date] {
let envelopes = try await client.apiClient.query( let envelopes = try await client.apiClient.query(
topic: .userIntro(client.address), topic: .userIntro(client.address),
@ -290,8 +302,25 @@ public class Conversations {
return seenPeers return seenPeers
} }
func listInvitations() async throws -> [SealedInvitation] { func listGroupInvitations() async throws -> [SealedInvitation] {
if !client.isGroupChatEnabled {
return []
}
let envelopes = try await client.apiClient.envelopes( let envelopes = try await client.apiClient.envelopes(
topic: Topic.groupInvite(client.address).description,
pagination: nil
)
return envelopes.compactMap { envelope in
// swiftlint:disable no_optional_try
try? SealedInvitation(serializedData: envelope.message)
// swiftlint:enable no_optional_try
}
}
func listInvitations() async throws -> [SealedInvitation] {
var envelopes = try await client.apiClient.envelopes(
topic: Topic.userInvite(client.address).description, topic: Topic.userInvite(client.address).description,
pagination: nil pagination: nil
) )

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
/// Decrypted messages from a conversation. /// Decrypted messages from a conversation.
public struct DecodedMessage { public struct DecodedMessage: Sendable {
public var id: String = "" public var id: String = ""
public var encodedContent: EncodedContent public var encodedContent: EncodedContent

View File

@ -0,0 +1,15 @@
//
// GroupChat.swift
//
//
// Created by Pat Nakajima on 6/11/23.
//
import Foundation
public struct GroupChat {
public static func registerCodecs() {
Client.register(codec: GroupChatMemberAddedCodec())
Client.register(codec: GroupChatTitleChangedCodec())
}
}

View File

@ -12,6 +12,7 @@ public enum Topic {
contact(String), contact(String),
userIntro(String), userIntro(String),
userInvite(String), userInvite(String),
groupInvite(String),
directMessageV1(String, String), directMessageV1(String, String),
directMessageV2(String) directMessageV2(String)
@ -25,6 +26,8 @@ public enum Topic {
return wrap("intro-\(address)") return wrap("intro-\(address)")
case let .userInvite(address): case let .userInvite(address):
return wrap("invite-\(address)") return wrap("invite-\(address)")
case let .groupInvite(address):
return wrap("groupInvite-\(address)")
case let .directMessageV1(address1, address2): case let .directMessageV1(address1, address2):
let addresses = [address1, address2].sorted().joined(separator: "-") let addresses = [address1, address2].sorted().joined(separator: "-")
return wrap("dm-\(addresses)") return wrap("dm-\(addresses)")

View File

@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
# #
spec.name = "XMTP" spec.name = "XMTP"
spec.version = "0.3.0-alpha0" spec.version = "0.3.1-alpha0"
spec.summary = "XMTP SDK Cocoapod" spec.summary = "XMTP SDK Cocoapod"
# This description is used to generate tags and improve search results. # This description is used to generate tags and improve search results.

View File

@ -495,6 +495,7 @@
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -534,6 +535,7 @@
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };

View File

@ -51,7 +51,7 @@
"location" : "https://github.com/kishikawakatsumi/KeychainAccess", "location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : { "state" : {
"branch" : "master", "branch" : "master",
"revision" : "6299daec1d74be12164fec090faf9ed14d0da9d6" "revision" : "ecb18d8ce4d88277cc4fb103973352d91e18c535"
} }
}, },
{ {
@ -176,8 +176,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/xmtp/xmtp-rust-swift", "location" : "https://github.com/xmtp/xmtp-rust-swift",
"state" : { "state" : {
"revision" : "41a1161cf06a86bab0aa886e450584a1191429b1", "revision" : "41a1161cf06a86bab0aa886e450584a1191429b1",
"version" : "0.3.0-beta0" "version" : "0.3.0-beta0"
} }
} }
], ],

View File

@ -62,7 +62,7 @@ class WCWalletConnection: WalletConnection, WalletConnectSwift.ClientDelegate {
walletConnectClient = WalletConnectSwift.Client(delegate: self, dAppInfo: dAppInfo) walletConnectClient = WalletConnectSwift.Client(delegate: self, dAppInfo: dAppInfo)
} }
func preferredConnectionMethod() throws -> WalletConnectionMethodType { @MainActor func preferredConnectionMethod() throws -> WalletConnectionMethodType {
guard let url = walletConnectURL?.asURL else { guard let url = walletConnectURL?.asURL else {
throw WalletConnectionError.walletConnectURL throw WalletConnectionError.walletConnectURL
} }