From 01eff435859c266f733585287815da843f25b481 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:55:28 +0400 Subject: [PATCH] mobile: group types (#808) --- .../java/chat/simplex/app/model/ChatModel.kt | 65 +++++++++++++-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 79 +++++++++++++++++++ apps/ios/SimpleXChat/APITypes.swift | 77 ++++++++++++++++++ apps/ios/SimpleXChat/ChatTypes.swift | 58 ++++++++++++-- 4 files changed, 266 insertions(+), 13 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index b911edc4a0..1b9b6a59c7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -476,6 +476,12 @@ class Profile( } } +@Serializable +class Group ( + val groupInfo: GroupInfo, + var members: List +) + @Serializable class GroupInfo ( val groupId: Long, @@ -520,27 +526,72 @@ class GroupProfile ( @Serializable class GroupMember ( val groupMemberId: Long, + val groupId: Long, val memberId: String, -// var memberRole: GroupMemberRole -// var memberCategory: GroupMemberCategory -// var memberStatus: GroupMemberStatus -// var invitedBy: InvitedBy + var memberRole: GroupMemberRole, + var memberCategory: GroupMemberCategory, + var memberStatus: GroupMemberStatus, + var invitedBy: InvitedBy, val localDisplayName: String, val memberProfile: Profile, - val memberContactId: Long? -// var activeConn: Connection? + val memberContactId: Long? = null, + var activeConn: Connection? = null ) { companion object { val sampleData = GroupMember( groupMemberId = 1, + groupId = 1, memberId = "abcd", + memberRole = GroupMemberRole.Member, + memberCategory = GroupMemberCategory.InviteeMember, + memberStatus = GroupMemberStatus.MemComplete, + invitedBy = InvitedBy.IBUser(), localDisplayName = "alice", memberProfile = Profile.sampleData, - memberContactId = 1 + memberContactId = 1, + activeConn = Connection.sampleData ) } } +@Serializable +enum class GroupMemberRole { + @SerialName("member") Member, + @SerialName("admin") Admin, + @SerialName("owner") Owner; +} + +@Serializable +enum class GroupMemberCategory { + @SerialName("user") UserMember, + @SerialName("invitee") InviteeMember, + @SerialName("host") HostMember, + @SerialName("pre") PreMember, + @SerialName("post") PostMember; +} + +@Serializable +enum class GroupMemberStatus { + @SerialName("removed") MemRemoved, + @SerialName("left") MemLeft, + @SerialName("deleted") MemGroupDeleted, + @SerialName("invited") MemInvited, + @SerialName("introduced") MemIntroduced, + @SerialName("intro-inv") MemIntroInvited, + @SerialName("accepted") MemAccepted, + @SerialName("announced") MemAnnounced, + @SerialName("connected") MemConnected, + @SerialName("complete") MemComplete, + @SerialName("creator") MemCreator; +} + +@Serializable +sealed class InvitedBy { + @Serializable @SerialName("contact") class IBContact(val byContactId: Long): InvitedBy() + @Serializable @SerialName("user") class IBUser: InvitedBy() + @Serializable @SerialName("unknown") class IBUnknown: InvitedBy() +} + @Serializable class LinkPreview ( val uri: String, diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a9b9ec8aa7..7f32bcba1b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -897,6 +897,13 @@ sealed class CC { class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() + class NewGroup(val groupProfile: GroupProfile): CC() + class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() + class ApiJoinGroup(val groupId: Long): CC() + // class ApiMemberRole(val groupId: Long, val memberId: Long, val memberRole: GroupMemberRole): CC() + class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() + class ApiLeaveGroup(val groupId: Long): CC() + class ApiListMembers(val groupId: Long): CC() class GetUserSMPServers(): CC() class SetUserSMPServers(val smpServers: List): CC() class AddContact: CC() @@ -935,6 +942,12 @@ sealed class CC { is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}" is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" + is NewGroup -> "/group ${groupProfile.displayName} ${groupProfile.fullName}" + is ApiAddMember -> "/_add #$groupId $contactId $memberRole" + is ApiJoinGroup -> "/_join #$groupId" + is ApiRemoveMember -> "/_remove #$groupId $memberId" + is ApiLeaveGroup -> "/_leave #$groupId" + is ApiListMembers -> "/_members #$groupId" is GetUserSMPServers -> "/smp_servers" is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is AddContact -> "/connect" @@ -974,6 +987,12 @@ sealed class CC { is ApiSendMessage -> "apiSendMessage" is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" + is NewGroup -> "newGroup" + is ApiAddMember -> "apiAddMember" + is ApiJoinGroup -> "apiJoinGroup" + is ApiRemoveMember -> "apiRemoveMember" + is ApiLeaveGroup -> "apiLeaveGroup" + is ApiListMembers -> "apiListMembers" is GetUserSMPServers -> "getUserSMPServers" is SetUserSMPServers -> "setUserSMPServers" is AddContact -> "addContact" @@ -1080,9 +1099,31 @@ sealed class CR { @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val deletedChatItem: AChatItem, val toChatItem: AChatItem): CR() + // group events + @Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact): CR() + @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR() + @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupMembers") class GroupMembers(val group: Group): CR() + @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() + @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("deletedMember") class DeletedMember(val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() + @Serializable @SerialName("leftMember") class LeftMember(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("groupDeleted") class GroupDeleted(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("contactsMerged") class ContactsMerged(val intoContact: Contact, val mergedContact: Contact): CR() + @Serializable @SerialName("groupInvitation") class GroupInvitation(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() + // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileStart") class RcvFileStart(val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val chatItem: AChatItem): CR() + // sending file events @Serializable @SerialName("sndFileStart") class SndFileStart(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @@ -1139,6 +1180,25 @@ sealed class CR { is ChatItemStatusUpdated -> "chatItemStatusUpdated" is ChatItemUpdated -> "chatItemUpdated" is ChatItemDeleted -> "chatItemDeleted" + is GroupCreated -> "groupCreated" + is SentGroupInvitation -> "sentGroupInvitation" + is UserAcceptedGroupSent -> "userAcceptedGroupSent" + is UserDeletedMember -> "userDeletedMember" + is LeftMemberUser -> "leftMemberUser" + is GroupMembers -> "groupMembers" + is ReceivedGroupInvitation -> "receivedGroupInvitation" + is GroupDeletedUser -> "groupDeletedUser" + is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" + is DeletedMemberUser -> "deletedMemberUser" + is DeletedMember -> "deletedMember" + is LeftMember -> "leftMember" + is GroupDeleted -> "groupDeleted" + is ContactsMerged -> "contactsMerged" + is GroupInvitation -> "groupInvitation" + is UserJoinedGroup -> "userJoinedGroup" + is JoinedGroupMember -> "joinedGroupMember" + is ConnectedToGroupMember -> "connectedToGroupMember" + is GroupRemoved -> "groupRemoved" is RcvFileAccepted -> "rcvFileAccepted" is RcvFileStart -> "rcvFileStart" is RcvFileComplete -> "rcvFileComplete" @@ -1199,6 +1259,25 @@ sealed class CR { is ChatItemStatusUpdated -> json.encodeToString(chatItem) is ChatItemUpdated -> json.encodeToString(chatItem) is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}" + is GroupCreated -> json.encodeToString(groupInfo) + is SentGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact" + is UserAcceptedGroupSent -> json.encodeToString(groupInfo) + is UserDeletedMember -> "groupInfo: $groupInfo\nmember: $member" + is LeftMemberUser -> json.encodeToString(groupInfo) + is GroupMembers -> json.encodeToString(group) + is ReceivedGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole" + is GroupDeletedUser -> json.encodeToString(groupInfo) + is JoinedGroupMemberConnecting -> "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member" + is DeletedMemberUser -> "groupInfo: $groupInfo\nmember: $member" + is DeletedMember -> "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember" + is LeftMember -> "groupInfo: $groupInfo\nmember: $member" + is GroupDeleted -> "groupInfo: $groupInfo\nmember: $member" + is ContactsMerged -> "intoContact: $intoContact\nmergedContact: $mergedContact" + is GroupInvitation -> json.encodeToString(groupInfo) + is UserJoinedGroup -> json.encodeToString(groupInfo) + is JoinedGroupMember -> "groupInfo: $groupInfo\nmember: $member" + is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member" + is GroupRemoved -> json.encodeToString(groupInfo) is RcvFileAccepted -> json.encodeToString(chatItem) is RcvFileStart -> json.encodeToString(chatItem) is RcvFileComplete -> json.encodeToString(chatItem) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b2622a539e..2a7847d7e1 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -33,6 +33,13 @@ public enum ChatCommand { case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiDeleteToken(token: DeviceToken) case apiGetNtfMessage(nonce: String, encNtfInfo: String) + case newGroup(groupProfile: GroupProfile) + case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) + case apiJoinGroup(groupId: Int64) + // case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole) + case apiRemoveMember(groupId: Int64, memberId: Int64) + case apiLeaveGroup(groupId: Int64) + case apiListMembers(groupId: Int64) case getUserSMPServers case setUserSMPServers(smpServers: [String]) case addContact @@ -83,6 +90,12 @@ public enum ChatCommand { case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" + case let .newGroup(groupProfile): return "/group \(groupProfile.displayName) \(groupProfile.fullName)" + case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" + case let .apiJoinGroup(groupId): return "/_join #\(groupId)" + case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)" + case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" + case let .apiListMembers(groupId): return "/_members #\(groupId)" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case .addContact: return "/connect" @@ -133,6 +146,12 @@ public enum ChatCommand { case .apiVerifyToken: return "apiVerifyToken" case .apiDeleteToken: return "apiDeleteToken" case .apiGetNtfMessage: return "apiGetNtfMessage" + case .newGroup: return "newGroup" + case .apiAddMember: return "apiAddMember" + case .apiJoinGroup: return "apiJoinGroup" + case .apiRemoveMember: return "apiRemoveMember" + case .apiLeaveGroup: return "apiLeaveGroup" + case .apiListMembers: return "apiListMembers" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .addContact: return "addContact" @@ -212,6 +231,26 @@ public enum ChatResponse: Decodable, Error { case chatItemStatusUpdated(chatItem: AChatItem) case chatItemUpdated(chatItem: AChatItem) case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem) + // group events + case groupCreated(groupInfo: GroupInfo) + case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact) + case userAcceptedGroupSent(groupInfo: GroupInfo) + case userDeletedMember(groupInfo: GroupInfo, member: GroupMember) + case leftMemberUser(groupInfo: GroupInfo) + case groupMembers(group: Group) + case receivedGroupInvitation(groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) + case groupDeletedUser(groupInfo: GroupInfo) + case joinedGroupMemberConnecting(groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) + case deletedMemberUser(groupInfo: GroupInfo, member: GroupMember) + case deletedMember(groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) + case leftMember(groupInfo: GroupInfo, member: GroupMember) + case groupDeleted(groupInfo: GroupInfo, member: GroupMember) + case contactsMerged(intoContact: Contact, mergedContact: Contact) + case groupInvitation(groupInfo: GroupInfo) + case userJoinedGroup(groupInfo: GroupInfo) + case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember) + case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember) + case groupRemoved(groupInfo: GroupInfo) // receiving file events case rcvFileAccepted(chatItem: AChatItem) case rcvFileStart(chatItem: AChatItem) @@ -278,6 +317,25 @@ public enum ChatResponse: Decodable, Error { case .chatItemStatusUpdated: return "chatItemStatusUpdated" case .chatItemUpdated: return "chatItemUpdated" case .chatItemDeleted: return "chatItemDeleted" + case .groupCreated: return "groupCreated" + case .sentGroupInvitation: return "sentGroupInvitation" + case .userAcceptedGroupSent: return "userAcceptedGroupSent" + case .userDeletedMember: return "userDeletedMember" + case .leftMemberUser: return "leftMemberUser" + case .groupMembers: return "groupMembers" + case .receivedGroupInvitation: return "receivedGroupInvitation" + case .groupDeletedUser: return "groupDeletedUser" + case .joinedGroupMemberConnecting: return "joinedGroupMemberConnecting" + case .deletedMemberUser: return "deletedMemberUser" + case .deletedMember: return "deletedMember" + case .leftMember: return "leftMember" + case .groupDeleted: return "groupDeleted" + case .contactsMerged: return "contactsMerged" + case .groupInvitation: return "groupInvitation" + case .userJoinedGroup: return "userJoinedGroup" + case .joinedGroupMember: return "joinedGroupMember" + case .connectedToGroupMember: return "connectedToGroupMember" + case .groupRemoved: return "groupRemoved" case .rcvFileAccepted: return "rcvFileAccepted" case .rcvFileStart: return "rcvFileStart" case .rcvFileComplete: return "rcvFileComplete" @@ -345,6 +403,25 @@ public enum ChatResponse: Decodable, Error { case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem) case let .chatItemUpdated(chatItem): return String(describing: chatItem) case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))" + case let .groupCreated(groupInfo): return String(describing: groupInfo) + case let .sentGroupInvitation(groupInfo, contact): return "groupInfo: \(groupInfo)\ncontact: \(contact)" + case let .userAcceptedGroupSent(groupInfo): return String(describing: groupInfo) + case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .leftMemberUser(groupInfo): return String(describing: groupInfo) + case let .groupMembers(group): return String(describing: group) + case let .receivedGroupInvitation(groupInfo, contact, memberRole): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)" + case let .groupDeletedUser(groupInfo): return String(describing: groupInfo) + case let .joinedGroupMemberConnecting(groupInfo, hostMember, member): return "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)" + case let .deletedMemberUser(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .deletedMember(groupInfo, byMember, deletedMember): return "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)" + case let .leftMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .groupDeleted(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .contactsMerged(intoContact, mergedContact): return "intoContact: \(intoContact)\nmergedContact: \(mergedContact)" + case let .groupInvitation(groupInfo): return String(describing: groupInfo) + case let .userJoinedGroup(groupInfo): return String(describing: groupInfo) + case let .joinedGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" + case let .groupRemoved(groupInfo): return String(describing: groupInfo) case let .rcvFileAccepted(chatItem): return String(describing: chatItem) case let .rcvFileStart(chatItem): return String(describing: chatItem) case let .rcvFileComplete(chatItem): return String(describing: chatItem) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 04bf5a50dd..1e400a766a 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -370,6 +370,11 @@ public enum ConnStatus: String, Decodable { } } +public struct Group: Decodable { + var groupInfo: GroupInfo + var members: [GroupMember] +} + public struct GroupInfo: Identifiable, Decodable, NamedChat { var groupId: Int64 var localDisplayName: GroupName @@ -406,15 +411,16 @@ public struct GroupProfile: Codable, NamedChat { public struct GroupMember: Decodable { public var groupMemberId: Int64 + var groupId: Int64 var memberId: String -// var memberRole: GroupMemberRole -// var memberCategory: GroupMemberCategory -// var memberStatus: GroupMemberStatus -// var invitedBy: InvitedBy + var memberRole: GroupMemberRole + var memberCategory: GroupMemberCategory + var memberStatus: GroupMemberStatus + var invitedBy: InvitedBy var localDisplayName: ContactName public var memberProfile: Profile var memberContactId: Int64? -// var activeConn: Connection? + var activeConn: Connection? var directChatId: ChatId? { get { @@ -435,13 +441,53 @@ public struct GroupMember: Decodable { public static let sampleData = GroupMember( groupMemberId: 1, + groupId: 1, memberId: "abcd", + memberRole: .admin, + memberCategory: .inviteeMember, + memberStatus: .memComplete, + invitedBy: .user, localDisplayName: "alice", memberProfile: Profile.sampleData, - memberContactId: 1 + memberContactId: 1, + activeConn: Connection.sampleData ) } +public enum GroupMemberRole: String, Decodable { + case member = "member" + case admin = "admin" + case owner = "owner" +} + +public enum GroupMemberCategory: String, Decodable { + case userMember = "user" + case inviteeMember = "invitee" + case hostMember = "host" + case preMember = "pre" + case postMember = "post" +} + +public enum GroupMemberStatus: String, Decodable { + case memRemoved = "removed" + case memLeft = "left" + case memGroupDeleted = "deleted" + case memInvited = "invited" + case memIntroduced = "introduced" + case memIntroInvited = "intro-inv" + case memAccepted = "accepted" + case memAnnounced = "announced" + case memConnected = "connected" + case memComplete = "complete" + case memCreator = "creator" +} + +public enum InvitedBy: Decodable { + case contact(byContactId: Int64) + case user + case unknown +} + public struct MemberSubError: Decodable { var member: GroupMember var memberError: ChatError