Compare commits
2 Commits
0.3.0-alph
...
main
Author | SHA1 | Date |
---|---|---|
![]() |
56c4f9f5d0 | |
![]() |
b5db26c75d |
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)")
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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";
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue