diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index ff3e78ec8b..d8709e3b50 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -31,6 +31,10 @@ final class ChatModel: ObservableObject { chats.first(where: { $0.id == id }) } + private func getChatIndex(_ id: String) -> Int? { + chats.firstIndex(where: { $0.id == id }) + } + func addChat(_ chat: Chat) { withAnimation { chats.insert(chat, at: 0) @@ -38,11 +42,26 @@ final class ChatModel: ObservableObject { } func updateChatInfo(_ cInfo: ChatInfo) { - if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) { + if let ix = getChatIndex(cInfo.id) { chats[ix].chatInfo = cInfo } } + func updateContact(_ contact: Contact) { + let cInfo = ChatInfo.direct(contact: contact) + if hasChat(contact.id) { + updateChatInfo(cInfo) + } else { + addChat(Chat(chatInfo: cInfo, chatItems: [])) + } + } + + func updateNetworkStatus(_ contact: Contact, _ status: Chat.NetworkStatus) { + if let ix = getChatIndex(contact.id) { + chats[ix].serverInfo.networkStatus = status + } + } + func replaceChat(_ id: String, _ chat: Chat) { if let ix = chats.firstIndex(where: { $0.id == id }) { chats[ix] = chat @@ -203,6 +222,39 @@ let sampleContactRequestChatInfo = ChatInfo.contactRequest(contactRequest: sampl final class Chat: ObservableObject, Identifiable { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] + @Published var serverInfo = ServerInfo(networkStatus: .unknown) + + struct ServerInfo: Decodable { + var networkStatus: NetworkStatus + } + + enum NetworkStatus: Decodable, Equatable { + case unknown + case connected + case disconnected + case error(String) + + var statusString: String { + get { + switch self { + case .connected: return "Connected to contact's server" + case let .error(err): return "Connecting to contact's server… (error: \(err))" + default: return "Connecting to contact's server…" + } + } + } + + var imageName: String { + get { + switch self { + case .unknown: return "circle.dotted" + case .connected: return "circle.fill" + case .disconnected: return "ellipsis.circle.fill" + case .error: return "exclamationmark.circle.fill" + } + } + } + } init(_ cData: ChatData) { self.chatInfo = cData.chatInfo @@ -231,10 +283,10 @@ struct Contact: Identifiable, Decodable { var activeConn: Connection var viaGroup: Int64? var createdAt: Date - + var id: String { get { "@\(contactId)" } } var apiId: Int64 { get { contactId } } - var connected: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } } + var ready: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } } } let sampleContact = Contact( diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9d86361960..58d47066f3 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -85,6 +85,9 @@ enum ChatResponse: Decodable, Error { case acceptingContactRequest(contact: Contact) case contactRequestRejected case contactUpdated(toContact: Contact) + case contactSubscribed(contact: Contact) + case contactDisconnected(contact: Contact) + case contactSubError(contact: Contact, chatError: ChatError) case newChatItem(chatItem: AChatItem) case chatCmdError(chatError: ChatError) @@ -108,6 +111,9 @@ enum ChatResponse: Decodable, Error { case .acceptingContactRequest: return "acceptingContactRequest" case .contactRequestRejected: return "contactRequestRejected" case .contactUpdated: return "contactUpdated" + case .contactSubscribed: return "contactSubscribed" + case .contactDisconnected: return "contactDisconnected" + case .contactSubError: return "contactSubError" case .newChatItem: return "newChatItem" case .chatCmdError: return "chatCmdError" } @@ -134,6 +140,9 @@ enum ChatResponse: Decodable, Error { case let .acceptingContactRequest(contact): return String(describing: contact) case .contactRequestRejected: return noDetails case let .contactUpdated(toContact): return String(describing: toContact) + case let .contactSubscribed(contact): return String(describing: contact) + case let .contactDisconnected(contact): return String(describing: contact) + case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))" case let .newChatItem(chatItem): return String(describing: chatItem) case let .chatCmdError(chatError): return String(describing: chatError) } @@ -299,12 +308,8 @@ func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) { chatModel.terminalItems.append(.resp(.now, res)) switch res { case let .contactConnected(contact): - let cInfo = ChatInfo.direct(contact: contact) - if chatModel.hasChat(contact.id) { - chatModel.updateChatInfo(cInfo) - } else { - chatModel.addChat(Chat(chatInfo: cInfo, chatItems: [])) - } + chatModel.updateContact(contact) + chatModel.updateNetworkStatus(contact, .connected) case let .receivedContactRequest(contactRequest): chatModel.addChat(Chat( chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), @@ -315,6 +320,21 @@ func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) { if chatModel.hasChat(toContact.id) { chatModel.updateChatInfo(cInfo) } + case let .contactSubscribed(contact): + chatModel.updateContact(contact) + chatModel.updateNetworkStatus(contact, .connected) + case let .contactDisconnected(contact): + chatModel.updateContact(contact) + chatModel.updateNetworkStatus(contact, .disconnected) + case let .contactSubError(contact, chatError): + chatModel.updateContact(contact) + var err: String + switch chatError { + case .errorAgent(agentError: .BROKER(brokerErr: .NETWORK)): err = "network" + case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" + default: err = String(describing: chatError) + } + chatModel.updateNetworkStatus(contact, .error(err)) case let .newChatItem(aChatItem): chatModel.addChatItem(aChatItem.chatInfo, aChatItem.chatItem) default: @@ -403,15 +423,135 @@ private func encodeCJSON(_ value: T) -> [CChar] { enum ChatError: Decodable { case error(errorType: ChatErrorType) + case errorMessage(errorMessage: String) + case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) - // TODO other error cases + case errorNotImplemented } enum ChatErrorType: Decodable { + case groupUserRole case invalidConnReq + case contactGroups(contact: Contact, groupNames: [GroupName]) + case groupContactRole(contactName: ContactName) + case groupDuplicateMember(contactName: ContactName) + case groupDuplicateMemberId + case groupNotJoined(groupInfo: GroupInfo) + case groupMemberNotActive + case groupMemberUserRemoved + case groupMemberNotFound(contactName: ContactName) + case groupMemberIntroNotFound(contactName: ContactName) + case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName) + case groupInternal(message: String) + case fileNotFound(message: String) + case fileAlreadyReceiving(message: String) + case fileAlreadyExists(filePath: String) + case fileRead(filePath: String, message: String) + case fileWrite(filePath: String, message: String) + case fileSend(fileId: Int64, agentError: String) + case fileRcvChunk(message: String) + case fileInternal(message: String) + case agentVersion + case commandError(message: String) } enum StoreError: Decodable { + case duplicateName + case contactNotFound(contactId: Int64) + case contactNotFoundByName(contactName: ContactName) + case contactNotReady(contactName: ContactName) + case duplicateContactLink case userContactLinkNotFound - // TODO other error cases + case contactRequestNotFound(contactRequestId: Int64) + case contactRequestNotFoundByName(contactName: ContactName) + case groupNotFound(groupId: Int64) + case groupNotFoundByName(groupName: GroupName) + case groupWithoutUser + case duplicateGroupMember + case groupAlreadyJoined + case groupInvitationNotFound + case sndFileNotFound(fileId: Int64) + case sndFileInvalid(fileId: Int64) + case rcvFileNotFound(fileId: Int64) + case fileNotFound(fileId: Int64) + case rcvFileInvalid(fileId: Int64) + case connectionNotFound(agentConnId: String) + case introNotFound + case uniqueID + case internalError(message: String) + case noMsgDelivery(connId: Int64, agentMsgId: String) + case badChatItem(itemId: Int64) + case chatItemNotFound(itemId: Int64) +} + +enum AgentErrorType: Decodable { + case CMD(cmdErr: CommandErrorType) + case CONN(connErr: ConnectionErrorType) + case SMP(smpErr: SMPErrorType) + case BROKER(brokerErr: BrokerErrorType) + case AGENT(agentErr: SMPAgentError) + case INTERNAL(internalErr: String) +} + +enum CommandErrorType: Decodable { + case PROHIBITED + case SYNTAX + case NO_CONN + case SIZE + case LARGE +} + +enum ConnectionErrorType: Decodable { + case NOT_FOUND + case DUPLICATE + case SIMPLEX + case NOT_ACCEPTED + case NOT_AVAILABLE +} + +enum BrokerErrorType: Decodable { + case RESPONSE(smpErr: SMPErrorType) + case UNEXPECTED + case NETWORK + case TRANSPORT(transportErr: SMPTransportError) + case TIMEOUT +} + +enum SMPErrorType: Decodable { + case BLOCK + case SESSION + case CMD(cmdErr: SMPCommandError) + case AUTH + case QUOTA + case NO_MSG + case LARGE_MSG + case INTERNAL +} + +enum SMPCommandError: Decodable { + case UNKNOWN + case SYNTAX + case NO_AUTH + case HAS_AUTH + case NO_QUEUE +} + +enum SMPTransportError: Decodable { + case TEBadBlock + case TELargeMsg + case TEBadSession + case TEHandshake(handshakeErr: SMPHandshakeError) +} + +enum SMPHandshakeError: Decodable { + case PARSE + case VERSION + case IDENTITY +} + +enum SMPAgentError: Decodable { + case A_MESSAGE + case A_PROHIBITED + case A_VERSION + case A_ENCRYPTION } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift new file mode 100644 index 0000000000..e1cee27f3a --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -0,0 +1,48 @@ +// +// ChatInfoView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 05/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatInfoView: View { + @ObservedObject var chat: Chat + + var body: some View { + VStack{ + ChatInfoImage(chat: chat) + .frame(width: 192, height: 192) + .padding(.top, 48) + .padding() + Text(chat.chatInfo.localDisplayName).font(.largeTitle) + .padding(.bottom, 2) + Text(chat.chatInfo.fullName).font(.title) + .padding(.bottom) + + if case .direct = chat.chatInfo { + HStack { + serverImage() + Text(chat.serverInfo.networkStatus.statusString) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } + + func serverImage() -> some View { + let status = chat.serverInfo.networkStatus + return Image(systemName: status.imageName) + .foregroundColor(status == .connected ? .green : .secondary) + } +} + +struct ChatInfoView_Previews: PreviewProvider { + var chatInfo = sampleDirectChatInfo + + static var previews: some View { + ChatInfoView(chat: Chat(chatInfo: sampleDirectChatInfo, chatItems: [])) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 04b40f4995..81ec993559 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -10,8 +10,9 @@ import SwiftUI struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - var chatInfo: ChatInfo + @ObservedObject var chat: Chat @State private var inProgress: Bool = false + @State private var showChatInfo = false var body: some View { VStack { @@ -33,7 +34,8 @@ struct ChatView: View { SendMessageView(sendMessage: sendMessage, inProgress: inProgress) } - .navigationTitle(chatInfo.chatViewName) + .navigationTitle(chat.chatInfo.chatViewName) + .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { chatModel.chatId = nil } label: { @@ -43,6 +45,25 @@ struct ChatView: View { } } } + ToolbarItem(placement: .principal) { + Button { + showChatInfo = true + } label: { + HStack { + ChatInfoImage(chat: chat) + .frame(width: 32, height: 32) + .padding(.trailing, 4) + VStack { + Text(chat.chatInfo.localDisplayName).font(.headline) + Text(chat.chatInfo.fullName).font(.subheadline) + } + } + .foregroundColor(.primary) + } + .sheet(isPresented: $showChatInfo) { + ChatInfoView(chat: chat) + } + } } .navigationBarBackButtonHidden(true) .onTapGesture { @@ -60,8 +81,8 @@ struct ChatView: View { func sendMessage(_ msg: String) { do { - let chatItem = try apiSendMessage(type: chatInfo.chatType, id: chatInfo.apiId, msg: .text(msg)) - chatModel.addChatItem(chatInfo, chatItem) + let chatItem = try apiSendMessage(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, msg: .text(msg)) + chatModel.addChatItem(chat.chatInfo, chatItem) } catch { print(error) } @@ -82,7 +103,7 @@ struct ChatView_Previews: PreviewProvider { chatItemSample(7, .directSnd, .now, "👍👍👍👍"), chatItemSample(8, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") ] - return ChatView(chatInfo: sampleDirectChatInfo) + return ChatView(chat: Chat(chatInfo: sampleDirectChatInfo, chatItems: [])) .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 7899627f55..5f82d44ae1 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -32,7 +32,7 @@ struct ChatListNavLink: View { } private func chatView() -> some View { - ChatView(chatInfo: chat.chatInfo) + ChatView(chat: chat) .onAppear { do { let cInfo = chat.chatInfo @@ -52,7 +52,7 @@ struct ChatListNavLink: View { destination: { chatView() }, label: { ChatPreviewView(chat: chat) } ) - .disabled(!contact.connected) + .disabled(!contact.ready) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { alertContact = contact diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 5f4244a98c..052637d92a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -13,18 +13,21 @@ struct ChatPreviewView: View { var body: some View { let cItem = chat.chatItems.last - var iconName: String - switch chat.chatInfo { - case .direct: iconName = "person.crop.circle.fill" - case .group: iconName = "person.2.circle.fill" - default: iconName = "circle.fill" - } return HStack(spacing: 8) { - Image(systemName: iconName) - .resizable() - .foregroundColor(Color(uiColor: .secondarySystemBackground)) - .frame(width: 63, height: 63) - .padding(.leading, 4) + ZStack(alignment: .bottomLeading) { + ChatInfoImage(chat: chat) + .frame(width: 63, height: 63) + if case .direct = chat.chatInfo, + chat.serverInfo.networkStatus == .connected { + Image(systemName: "circle.fill") + .resizable() + .foregroundColor(.green) + .frame(width: 5, height: 5) + .padding([.bottom, .leading], 1) + } + } + .padding(.leading, 4) + VStack(spacing: 0) { HStack(alignment: .top) { Text(chat.chatInfo.chatViewName) @@ -46,7 +49,7 @@ struct ChatPreviewView: View { .padding([.leading, .trailing], 8) .padding(.bottom, 4) } - else if case let .direct(contact) = chat.chatInfo, !contact.connected { + else if case let .direct(contact) = chat.chatInfo, !contact.ready { Text("Connecting...") .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) .padding([.leading, .trailing], 8) diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift new file mode 100644 index 0000000000..a50158a384 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -0,0 +1,33 @@ +// +// ChatInfoImage.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 05/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ChatInfoImage: View { + @ObservedObject var chat: Chat + + var body: some View { + var iconName: String + switch chat.chatInfo { + case .direct: iconName = "person.crop.circle.fill" + case .group: iconName = "person.2.circle.fill" + default: iconName = "circle.fill" + } + + return Image(systemName: iconName) + .resizable() + .foregroundColor(Color(uiColor: .secondarySystemBackground)) + } +} + +struct ChatInfoImage_Previews: PreviewProvider { + static var previews: some View { + ChatInfoImage(chat: Chat(chatInfo: sampleDirectChatInfo, chatItems: [])) + .previewLayout(.fixed(width: 63, height: 63)) + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 28a608d78d..5de14db513 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -35,6 +35,10 @@ 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; + 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; + 5C971E1E27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; + 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; + 5C971E2227AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; 5C9FD96B27A56D4D0075386C /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; }; 5C9FD96C27A56D4D0075386C /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96A27A56D4D0075386C /* JSON.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; @@ -118,6 +122,8 @@ 5C764E7E279C7275000C6508 /* SimpleX (macOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (macOS)-Bridging-Header.h"; sourceTree = ""; }; 5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; + 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; + 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = ""; }; @@ -194,6 +200,7 @@ 5C2E260D27A30E2400F70299 /* Views */ = { isa = PBXGroup; children = ( + 5C971E1F27AEBF7000C8A3CE /* Helpers */, 5C5F4AC227A5E9AF00B51EF1 /* Chat */, 5CB9250B27A942F300ACCCDD /* ChatList */, 5CB924DD27A8622200ACCCDD /* NewChat */, @@ -209,6 +216,7 @@ children = ( 5CE4407427ADB657007B033A /* ChatItem */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, + 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */, 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, 5CE4407127ADB1D0007B033A /* Emoji.swift */, @@ -247,6 +255,14 @@ path = Model; sourceTree = ""; }; + 5C971E1F27AEBF7000C8A3CE /* Helpers */ = { + isa = PBXGroup; + children = ( + 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 5CA059BD279559F40002BEB4 = { isa = PBXGroup; children = ( @@ -543,10 +559,12 @@ 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, + 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5CCD403727A5F9A200368C90 /* ConnectContactView.swift in Sources */, 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, + 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */, @@ -578,10 +596,12 @@ 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260C27A30CFA00F70299 /* ChatListView.swift in Sources */, + 5C971E2227AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5CA059EC279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5CCD403827A5F9A200368C90 /* ConnectContactView.swift in Sources */, 5CCD403B27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, 5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */, + 5C971E1E27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CC1C99627A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, 5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */, 5CB924D527A853F100ACCCDD /* SettingsButton.swift in Sources */,