Skip to content

Commit

Permalink
Groups a sub-type of Conversation (#246)
Browse files Browse the repository at this point in the history
* first pass at making groups extend conversations

* update groups to inherit from conversation

* include groups in conversations list

* add combined streaming

* fix up test

* write a test for it

* fix the return statement

* small clean up
  • Loading branch information
nplasterer authored Feb 14, 2024
1 parent 486e6f6 commit 0c7db3a
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 24 deletions.
84 changes: 78 additions & 6 deletions Sources/XMTPiOS/Conversation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import LibXMTP

public enum ConversationContainer: Codable {
case v1(ConversationV1Container), v2(ConversationV2Container)
Expand All @@ -23,10 +24,10 @@ public enum ConversationContainer: Codable {
/// Wrapper that provides a common interface between ``ConversationV1`` and ``ConversationV2`` objects.
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
case v1(ConversationV1), v2(ConversationV2)
case v1(ConversationV1), v2(ConversationV2), group(Group)

public enum Version {
case v1, v2
case v1, v2, group
}

public func consentState() async -> ConsentState {
Expand All @@ -37,6 +38,8 @@ public enum Conversation: Sendable {
client = conversationV1.client
case .v2(let conversationV2):
client = conversationV2.client
case let .group(group):
client = group.client
}

return await client.contacts.consentList.state(address: peerAddress)
Expand All @@ -48,6 +51,8 @@ public enum Conversation: Sendable {
return .v1
case .v2:
return .v2
case .group:
return .group
}
}

Expand All @@ -57,15 +62,19 @@ public enum Conversation: Sendable {
return conversationV1.sentAt
case let .v2(conversationV2):
return conversationV2.createdAt
case let .group(group):
return group.createdAt
}
}

public var encodedContainer: ConversationContainer {
public func encodedContainer() throws -> ConversationContainer {
switch self {
case let .v1(conversationV1):
return .v1(conversationV1.encodedContainer)
case let .v2(conversationV2):
return .v2(conversationV2.encodedContainer)
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -76,6 +85,27 @@ public enum Conversation: Sendable {
return conversationV1.peerAddress
case let .v2(conversationV2):
return conversationV2.peerAddress
case let .group(group):
var addresses = group.memberAddresses
if let index = addresses.firstIndex(of: clientAddress) {
addresses.remove(at: index)
}
return addresses.joined(separator: ",")
}
}

public var peerAddresses: [String] {
switch self {
case let .v1(conversationV1):
return [conversationV1.peerAddress]
case let .v2(conversationV2):
return [conversationV2.peerAddress]
case let .group(group):
var addresses = group.memberAddresses
if let index = addresses.firstIndex(of: clientAddress) {
addresses.remove(at: index)
}
return addresses
}
}

Expand All @@ -85,6 +115,8 @@ public enum Conversation: Sendable {
return nil
case let .v2(conversationV2):
return conversationV2.keyMaterial
case let .group(group):
return nil
}
}

Expand All @@ -97,6 +129,8 @@ public enum Conversation: Sendable {
return nil
case let .v2(conversation):
return conversation.context.conversationID
case let .group(group):
return nil
}
}

Expand All @@ -118,21 +152,31 @@ public enum Conversation: Sendable {
}
}

public func decode(_ envelope: Envelope) throws -> DecodedMessage {
public func decode(_ envelope: Envelope, message: FfiMessage? = nil) throws -> DecodedMessage {
switch self {
case let .v1(conversationV1):
return try conversationV1.decode(envelope: envelope)
case let .v2(conversationV2):
return try conversationV2.decode(envelope: envelope)
case let .group(group):
guard let messageDecoded = try message?.fromFFI(client: client) else {
throw GroupError.groupsRequireMessagePassed
}
return messageDecoded
}
}

public func decrypt(_ envelope: Envelope) throws -> DecryptedMessage {
public func decrypt(_ envelope: Envelope, message: FfiMessage? = nil) throws -> DecryptedMessage {
switch self {
case let .v1(conversationV1):
return try conversationV1.decrypt(envelope: envelope)
case let .v2(conversationV2):
return try conversationV2.decrypt(envelope: envelope)
case let .group(group):
guard let messageDecrypted = try message?.fromFFIDecrypted(client: client) else {
throw GroupError.groupsRequireMessagePassed
}
return messageDecrypted
}
}

Expand All @@ -142,6 +186,8 @@ public enum Conversation: Sendable {
throw RemoteAttachmentError.v1NotSupported
case let .v2(conversationV2):
return try await conversationV2.encode(codec: codec, content: content)
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -151,6 +197,8 @@ public enum Conversation: Sendable {
return try await conversationV1.prepareMessage(encodedContent: encodedContent, options: options)
case let .v2(conversationV2):
return try await conversationV2.prepareMessage(encodedContent: encodedContent, options: options)
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -160,6 +208,8 @@ public enum Conversation: Sendable {
return try await conversationV1.prepareMessage(content: content, options: options ?? .init())
case let .v2(conversationV2):
return try await conversationV2.prepareMessage(content: content, options: options ?? .init())
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -171,6 +221,8 @@ public enum Conversation: Sendable {
return try await conversationV1.send(prepared: prepared)
case let .v2(conversationV2):
return try await conversationV2.send(prepared: prepared)
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -180,6 +232,8 @@ public enum Conversation: Sendable {
return try await conversationV1.send(content: content, options: options)
case let .v2(conversationV2):
return try await conversationV2.send(content: content, options: options)
case let .group(group):
return try await group.send(content: content, options: options)
}
}

Expand All @@ -189,6 +243,8 @@ public enum Conversation: Sendable {
return try await conversationV1.send(encodedContent: encodedContent, options: options)
case let .v2(conversationV2):
return try await conversationV2.send(encodedContent: encodedContent, options: options)
case let .group(group):
return try await group.send(content: encodedContent, options: options)
}
}

Expand All @@ -199,6 +255,8 @@ public enum Conversation: Sendable {
return try await conversationV1.send(content: text, options: options)
case let .v2(conversationV2):
return try await conversationV2.send(content: text, options: options)
case let .group(group):
return try await group.send(content: text, options: options)
}
}

Expand All @@ -213,15 +271,19 @@ public enum Conversation: Sendable {
return conversation.topic.description
case let .v2(conversation):
return conversation.topic
case let .group(group):
return group.id.toHex
}
}

public func streamEphemeral() -> AsyncThrowingStream<Envelope, Error>? {
public func streamEphemeral() throws -> AsyncThrowingStream<Envelope, Error>? {
switch self {
case let .v1(conversation):
return conversation.streamEphemeral()
case let .v2(conversation):
return conversation.streamEphemeral()
case let .group(group):
throw GroupError.notSupportedByGroups
}
}

Expand All @@ -235,6 +297,8 @@ public enum Conversation: Sendable {
return conversation.streamMessages()
case let .v2(conversation):
return conversation.streamMessages()
case let .group(group):
return group.streamMessages()
}
}

Expand All @@ -244,6 +308,8 @@ public enum Conversation: Sendable {
return conversation.streamDecryptedMessages()
case let .v2(conversation):
return conversation.streamDecryptedMessages()
case let .group(group):
return group.streamDecryptedMessages()
}
}

Expand All @@ -254,6 +320,8 @@ public enum Conversation: Sendable {
return try await conversationV1.messages(limit: limit, before: before, after: after, direction: direction)
case let .v2(conversationV2):
return try await conversationV2.messages(limit: limit, before: before, after: after, direction: direction)
case let .group(group):
return try await group.messages(before: before, after: after, limit: limit, direction: direction)
}
}

Expand All @@ -263,6 +331,8 @@ public enum Conversation: Sendable {
return try await conversationV1.decryptedMessages(limit: limit, before: before, after: after, direction: direction)
case let .v2(conversationV2):
return try await conversationV2.decryptedMessages(limit: limit, before: before, after: after, direction: direction)
case let .group(group):
return try await group.decryptedMessages(before: before, after: after, limit: limit, direction: direction)
}
}

Expand All @@ -272,6 +342,8 @@ public enum Conversation: Sendable {
return conversationV1.client
case let .v2(conversationV2):
return conversationV2.client
case let .group(group):
return group.client
}
}
}
Expand Down
46 changes: 43 additions & 3 deletions Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public enum ConversationError: Error, CustomStringConvertible {
}

public enum GroupError: Error, CustomStringConvertible {
case alphaMLSNotEnabled, emptyCreation, memberCannotBeSelf, memberNotRegistered([String])
case alphaMLSNotEnabled, emptyCreation, memberCannotBeSelf, memberNotRegistered([String]), groupsRequireMessagePassed, notSupportedByGroups

public var description: String {
switch self {
Expand All @@ -29,6 +29,10 @@ public enum GroupError: Error, CustomStringConvertible {
return "GroupError.memberCannotBeSelf you cannot add yourself to a group"
case .memberNotRegistered(let array):
return "GroupError.memberNotRegistered members not registered: \(array.joined(separator: ", "))"
case .groupsRequireMessagePassed:
return "GroupError.groupsRequireMessagePassed you cannot call this method without passing a message instead of an envelope"
case .notSupportedByGroups:
return "GroupError.notSupportedByGroups this method is not supported by groups"
}
}
}
Expand Down Expand Up @@ -98,6 +102,18 @@ public actor Conversations {
}
}
}

private func streamGroupConversations() -> AsyncThrowingStream<Conversation, Error> {
AsyncThrowingStream { continuation in
Task {
self.streamHolder.stream = try await self.client.v3Client?.conversations().stream(
callback: GroupStreamCallback(client: self.client) { group in
continuation.yield(Conversation.group(group))
}
)
}
}
}

public func newGroup(with addresses: [String]) async throws -> Group {
guard let v3Client = client.v3Client else {
Expand Down Expand Up @@ -393,15 +409,39 @@ public actor Conversations {
}
}
}


public func streamAll() -> AsyncThrowingStream<Conversation, Error> {
AsyncThrowingStream<Conversation, Error> { continuation in
Task {
do {
for try await conversation in streamGroupConversations() {
continuation.yield(conversation)
}
for try await conversation in stream() {
continuation.yield(conversation)
}
} catch {
continuation.finish(throwing: error)
}
}
}
}
private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 {
let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys)
let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)

return conversation
}

public func list() async throws -> [Conversation] {
public func list(includeGroups: Bool = false) async throws -> [Conversation] {
if (includeGroups) {
try await sync()
let groups = try await groups()

groups.forEach { group in
conversationsByTopic[group.id.toHex] = Conversation.group(group)
}
}
var newConversations: [Conversation] = []
let mostRecent = conversationsByTopic.values.max { a, b in
a.createdAt < b.createdAt
Expand Down
Loading

0 comments on commit 0c7db3a

Please sign in to comment.