contact and server connection info (#271)

This commit is contained in:
Evgeny Poberezkin 2022-02-05 20:10:47 +00:00 committed by GitHub
parent 3d137995d8
commit 67dbdcd257
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 347 additions and 30 deletions

View File

@ -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(

View File

@ -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<T: Encodable>(_ 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
}

View File

@ -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: []))
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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 = "<group>"; };
5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
5C971E1F27AEBF7000C8A3CE /* Helpers */ = {
isa = PBXGroup;
children = (
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
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 */,