diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 0aa653ab2e..193b675a57 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1053,7 +1053,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest, chat_: ChatData?) case contactUpdated(user: UserRef, toContact: Contact) case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) - case networkStatus(networkStatus: NetworkStatus, connections: [String]) + case subscriptionStatus(subscriptionStatus: SubscriptionStatus, connections: [String]) case chatInfoUpdated(user: UserRef, chatInfo: ChatInfo) case newChatItems(user: UserRef, chatItems: [AChatItem]) case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) @@ -1130,7 +1130,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case .receivedContactRequest: "receivedContactRequest" case .contactUpdated: "contactUpdated" case .groupMemberUpdated: "groupMemberUpdated" - case .networkStatus: "networkStatus" + case .subscriptionStatus: "subscriptionStatus" case .chatInfoUpdated: "chatInfoUpdated" case .newChatItems: "newChatItems" case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" @@ -1202,7 +1202,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case let .receivedContactRequest(u, contactRequest, chat_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\nchat_: \(String(describing: chat_))") case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)") - case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" + case let .subscriptionStatus(status, conns): return "subscriptionStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" case let .chatInfoUpdated(u, chatInfo): return withUser(u, String(describing: chatInfo)) case let .newChatItems(u, chatItems): let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") @@ -1362,38 +1362,6 @@ enum ChatDeleteMode: Codable { } } -enum NetworkStatus: Decodable, Equatable { - case unknown - case connected - case disconnected - case error(connectionError: String) - - var statusString: LocalizedStringKey { - switch self { - case .connected: "connected" - case .error: "error" - default: "connecting" - } - } - - var statusExplanation: LocalizedStringKey { - switch self { - case .connected: "You are connected to the server used to receive messages from this contact." - case let .error(err): "Trying to connect to the server used to receive messages from this contact (error: \(err))." - default: "Trying to connect to the server used to receive messages from this contact." - } - } - - var imageName: String { - switch self { - case .unknown: "circle.dotted" - case .connected: "circle.fill" - case .disconnected: "ellipsis.circle.fill" - case .error: "exclamationmark.circle.fill" - } - } -} - enum ForwardConfirmation: Decodable, Hashable { case filesNotAccepted(fileIds: [Int64]) case filesInProgress(filesCount: Int) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 032e8e67d6..5ebab167fd 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -351,6 +351,8 @@ final class ChatModel: ObservableObject { @Published var deletedChats: Set = [] // current chat @Published var chatId: String? + @Published var chatAgentConnId: String? + @Published var chatSubStatus: SubscriptionStatus? @Published var openAroundItemId: ChatItem.ID? = nil @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 84b952915d..24d0b58f54 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2322,9 +2322,12 @@ func processReceivedMsg(_ res: ChatEvent) async { _ = m.upsertGroupMember(groupInfo, toMember) } } - case let .networkStatus(status, connections): - // TODO [sub status] update status for current chat in model if id matches - return + case let .subscriptionStatus(status, connections): + if let chatAgentConnId = m.chatAgentConnId, connections.contains(chatAgentConnId) { + await MainActor.run { + m.chatSubStatus = status + } + } case let .chatInfoUpdated(user, chatInfo): if active(user) { await MainActor.run { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 37255e652a..902f61087d 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -114,7 +114,7 @@ struct ChatInfoView: View { enum ChatInfoViewAlert: Identifiable { case clearChatAlert - case networkStatusAlert + case subStatusAlert(status: SubscriptionStatus) case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert @@ -125,7 +125,7 @@ struct ChatInfoView: View { var id: String { switch self { case .clearChatAlert: return "clearChatAlert" - case .networkStatusAlert: return "networkStatusAlert" + case let .subStatusAlert(status): return "subStatusAlert \(status)" case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" @@ -235,10 +235,12 @@ struct ChatInfoView: View { if contact.ready && contact.active { Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { - networkStatusRow() - .onTapGesture { - alert = .networkStatusAlert - } + if let chatSubStatus = chatModel.chatSubStatus { + subStatusRow(chatSubStatus) + .onTapGesture { + alert = .subStatusAlert(status: chatSubStatus) + } + } if let connStats = connectionStats { Button("Change receiving address") { alert = .switchAddressAlert @@ -324,7 +326,7 @@ struct ChatInfoView: View { .alert(item: $alert) { alertItem in switch(alertItem) { case .clearChatAlert: return clearChatAlert() - case .networkStatusAlert: return networkStatusAlert() + case let .subStatusAlert(status): return subStatusAlert(status) case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: @@ -545,26 +547,22 @@ struct ChatInfoView: View { } } - private func networkStatusRow() -> some View { + private func subStatusRow(_ status: SubscriptionStatus) -> some View { HStack { Text("Network status") Image(systemName: "info.circle") .foregroundColor(theme.colors.primary) .font(.system(size: 14)) Spacer() - // TODO [sub status] use status from model - // Text(networkModel.contactNetworkStatus(contact).statusString) - Text(NetworkStatus.connected.statusString) + Text(status.statusString) .foregroundColor(theme.colors.secondary) - serverImage() + serverImage(status) } } - private func serverImage() -> some View { - // TODO [sub status] use status from model - let status = NetworkStatus.connected // networkModel.contactNetworkStatus(contact) + private func serverImage(_ status: SubscriptionStatus) -> some View { return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : theme.colors.secondary) + .foregroundColor(status == .active ? .green : theme.colors.secondary) .font(.system(size: 12)) } @@ -607,11 +605,10 @@ struct ChatInfoView: View { ) } - private func networkStatusAlert() -> Alert { + private func subStatusAlert(_ status: SubscriptionStatus) -> Alert { Alert( title: Text("Network status"), - // TODO [sub status] use status from model - message: Text("") // Text(networkModel.contactNetworkStatus(contact).statusExplanation) + message: Text(status.statusExplanation) ) } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index fa53045391..2270e9309d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -393,6 +393,8 @@ struct ChatView: View { if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { + chatModel.chatAgentConnId = nil + chatModel.chatSubStatus = nil im.reversedChatItems = [] im.chatState.clear() chatModel.groupMembers = [] @@ -657,18 +659,30 @@ struct ChatView: View { private func initChatView() { let cInfo = chat.chatInfo // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. - if case .active = scenePhase, - case let .direct(contact) = cInfo { - Task { - do { - let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) - await MainActor.run { - if let s = stats { - chatModel.updateContactConnectionStats(contact, s) + if case .active = scenePhase { + if case let .direct(contact) = cInfo { + Task { + do { + let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) + await MainActor.run { + if let s = stats { + chatModel.updateContactConnectionStats(contact, s) + if let contactConn = contact.activeConn { + chatModel.chatAgentConnId = contactConn.agentConnId + chatModel.chatSubStatus = s.subStatus + } + } } + } catch let error { + logger.error("apiContactInfo error: \(responseError(error))") + } + } + } else { + Task { + await MainActor.run { + chatModel.chatAgentConnId = nil + chatModel.chatSubStatus = nil } - } catch let error { - logger.error("apiContactInfo error: \(responseError(error))") } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1519c99420..50022e3646 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -539,13 +539,13 @@ public enum MsgFilter: String, Codable, Hashable { } } -// TODO [sub status] add SubscriptionStatus, QueueStatus public struct ConnectionStats: Decodable, Hashable { public var connAgentVersion: Int public var rcvQueuesInfo: [RcvQueueInfo] public var sndQueuesInfo: [SndQueueInfo] public var ratchetSyncState: RatchetSyncState public var ratchetSyncSupported: Bool + public var subStatus: SubscriptionStatus? public var ratchetSyncAllowed: Bool { ratchetSyncSupported && [.allowed, .required].contains(ratchetSyncState) @@ -560,25 +560,28 @@ public struct ConnectionStats: Decodable, Hashable { } } -public struct RcvQueueInfo: Codable, Hashable { +public struct RcvQueueInfo: Decodable, Hashable { public var rcvServer: String + public var status: QueueStatus public var rcvSwitchStatus: RcvSwitchStatus? public var canAbortSwitch: Bool + public var subStatus: SubscriptionStatus } -public enum RcvSwitchStatus: String, Codable, Hashable { +public enum RcvSwitchStatus: String, Decodable, Hashable { case switchStarted = "switch_started" case sendingQADD = "sending_qadd" case sendingQUSE = "sending_quse" case receivedMessage = "received_message" } -public struct SndQueueInfo: Codable, Hashable { +public struct SndQueueInfo: Decodable, Hashable { public var sndServer: String + public var status: QueueStatus public var sndSwitchStatus: SndSwitchStatus? } -public enum SndSwitchStatus: String, Codable, Hashable { +public enum SndSwitchStatus: String, Decodable, Hashable { case sendingQKEY = "sending_qkey" case sendingQTEST = "sending_qtest" } @@ -607,6 +610,48 @@ public enum RatchetSyncState: String, Decodable { case agreed } +public enum QueueStatus: String, Decodable, Hashable { + case new + case confirmed + case secured + case active + case disabled +} + +public enum SubscriptionStatus: Decodable, Hashable { + case active + case pending + case removed(subError: String) + case noSub + + public var statusString: LocalizedStringKey { + switch self { + case .active: "connected" + case .pending: "connecting" + case .removed: "error" + case .noSub: "no subscription" + } + } + + public var statusExplanation: LocalizedStringKey { + switch self { + case .active: "You are connected to the server used to receive messages from this contact." + case .pending: "Trying to connect to the server used to receive messages from this contact." + case let .removed(err): "Error connecting to the server used to receive messages from this contact: \(err)." + case .noSub: "You are not connected to the server used to receive messages from this contact (no subscription)." + } + } + + public var imageName: String { + switch self { + case .active: "circle.fill" + case .pending: "ellipsis.circle.fill" + case .removed: "exclamationmark.circle.fill" + case .noSub: "circle.dotted" + } + } +} + public protocol SelectableItem: Identifiable, Equatable { var label: LocalizedStringKey { get } static var values: [Self] { get }