From a65151ba6db50f654f459a1fd2af1ca76e4cfc42 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:53:47 +0000 Subject: [PATCH] core, ui: replace map of network statuses with subscription status of current chat (#6353) * core: subscription status wip * update * update * update * remove statuses core * cleanup ios * comment * plans * remove NetworkStatus * ios wip * contact sub status * Revert "contact sub status" This reverts commit 50cf94beede94eafc12a06002dec2e98042d05f0. * sub status * set on connected * kotlin * rename * layout * member status * kotlin * fix chat subscription status * string * core: update simplexmq * client notices * update simplexmq * update alert * update simplexmq * android/desktop * formatting * fix tests * update plans and API docs --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/AppAPITypes.swift | 52 +----- apps/ios/Shared/Model/ChatModel.swift | 25 +-- apps/ios/Shared/Model/SimpleXAPI.swift | 88 +++++----- .../Shared/Views/Chat/ChatInfoToolbar.swift | 32 ++++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 65 +++---- apps/ios/Shared/Views/Chat/ChatView.swift | 34 ++-- .../Chat/ComposeMessage/ComposeView.swift | 2 - .../Chat/Group/GroupMemberInfoView.swift | 11 +- .../Views/ChatList/ChatPreviewView.swift | 30 ---- .../Views/UserSettings/UserAddressView.swift | 7 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/APITypes.swift | 55 +++++- .../chat/simplex/common/model/ChatModel.kt | 42 +---- .../chat/simplex/common/model/SimpleXAPI.kt | 164 +++++++++++------- .../simplex/common/views/chat/ChatInfoView.kt | 40 ++--- .../simplex/common/views/chat/ChatView.kt | 67 ++++++- ...ContextGroupDirectInvitationActionsView.kt | 1 - .../simplex/common/views/chat/ComposeView.kt | 2 - .../views/chat/group/GroupMemberInfoView.kt | 12 +- .../views/chatlist/ChatListNavLinkView.kt | 10 +- .../common/views/chatlist/ChatPreviewView.kt | 31 +--- .../views/chatlist/ServersSummaryView.kt | 14 +- .../networkAndServers/OperatorView.kt | 3 +- .../commonMain/resources/MR/ar/strings.xml | 2 +- .../commonMain/resources/MR/base/strings.xml | 8 +- .../commonMain/resources/MR/bg/strings.xml | 2 +- .../commonMain/resources/MR/ca/strings.xml | 2 +- .../commonMain/resources/MR/cs/strings.xml | 2 +- .../commonMain/resources/MR/da/strings.xml | 2 +- .../commonMain/resources/MR/de/strings.xml | 2 +- .../commonMain/resources/MR/es/strings.xml | 2 +- .../commonMain/resources/MR/fa/strings.xml | 2 +- .../commonMain/resources/MR/fi/strings.xml | 2 +- .../commonMain/resources/MR/fr/strings.xml | 2 +- .../commonMain/resources/MR/hu/strings.xml | 2 +- .../commonMain/resources/MR/in/strings.xml | 2 +- .../commonMain/resources/MR/it/strings.xml | 2 +- .../commonMain/resources/MR/iw/strings.xml | 2 +- .../commonMain/resources/MR/ja/strings.xml | 2 +- .../commonMain/resources/MR/lt/strings.xml | 2 +- .../commonMain/resources/MR/nl/strings.xml | 2 +- .../commonMain/resources/MR/pl/strings.xml | 2 +- .../resources/MR/pt-rBR/strings.xml | 2 +- .../commonMain/resources/MR/ro/strings.xml | 2 +- .../commonMain/resources/MR/ru/strings.xml | 2 +- .../commonMain/resources/MR/th/strings.xml | 2 +- .../commonMain/resources/MR/tr/strings.xml | 2 +- .../commonMain/resources/MR/uk/strings.xml | 2 +- .../commonMain/resources/MR/vi/strings.xml | 2 +- .../resources/MR/zh-rCN/strings.xml | 2 +- .../resources/MR/zh-rTW/strings.xml | 2 +- bots/api/TYPES.md | 16 ++ bots/src/API/Docs/Commands.hs | 1 - bots/src/API/Docs/Events.hs | 2 +- bots/src/API/Docs/Responses.hs | 1 - bots/src/API/Docs/Types.hs | 3 + cabal.project | 2 +- .../types/typescript/src/types.ts | 14 ++ scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 1 - src/Simplex/Chat.hs | 4 +- src/Simplex/Chat/Controller.hs | 11 +- src/Simplex/Chat/Library/Commands.hs | 16 +- src/Simplex/Chat/Library/Subscriber.hs | 14 +- src/Simplex/Chat/Messages.hs | 1 - src/Simplex/Chat/Operators.hs | 1 - .../SQLite/Migrations/agent_query_plans.txt | 26 ++- src/Simplex/Chat/Types.hs | 25 --- src/Simplex/Chat/Types/UITheme.hs | 1 - src/Simplex/Chat/Types/Util.hs | 8 - src/Simplex/Chat/View.hs | 16 +- tests/ChatTests/Direct.hs | 19 +- tests/JSONFixtures.hs | 6 - tests/JSONTests.hs | 1 - 74 files changed, 529 insertions(+), 527 deletions(-) delete mode 100644 src/Simplex/Chat/Types/Util.hs diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 85b0b9a1dc..193b675a57 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -158,7 +158,6 @@ enum ChatCommand: ChatCmdProtocol { case apiGetCallInvitations case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) // WebRTC calls / - case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) @@ -359,7 +358,6 @@ enum ChatCommand: ChatCmdProtocol { case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" case .apiGetCallInvitations: return "/_call get" case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" - case .apiGetNetworkStatuses: return "/_network_statuses" case let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))" case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(onOff(unreadChat))" @@ -534,7 +532,6 @@ enum ChatCommand: ChatCmdProtocol { case .apiEndCall: return "apiEndCall" case .apiGetCallInvitations: return "apiGetCallInvitations" case .apiCallStatus: return "apiCallStatus" - case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" case .apiChatRead: return "apiChatRead" case .apiChatItemsRead: return "apiChatItemsRead" case .apiChatUnread: return "apiChatUnread" @@ -793,7 +790,6 @@ enum ChatResponse1: Decodable, ChatAPIResult { case userContactLinkDeleted(user: User) case acceptingContactRequest(user: UserRef, contact: Contact) case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?) - case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) case newChatItems(user: UserRef, chatItems: [AChatItem]) case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?) @@ -837,7 +833,6 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .userContactLinkDeleted: "userContactLinkDeleted" case .acceptingContactRequest: "acceptingContactRequest" case .contactRequestRejected: "contactRequestRejected" - case .networkStatuses: "networkStatuses" case .newChatItems: "newChatItems" case .groupChatItemsDeleted: "groupChatItemsDeleted" case .forwardPlan: "forwardPlan" @@ -870,7 +865,6 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .userContactLinkDeleted: return noDetails case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) case let .contactRequestRejected(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))") - case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) case let .newChatItems(u, chatItems): let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") return withUser(u, itemsString) @@ -1059,8 +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 networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case subscriptionStatus(subscriptionStatus: SubscriptionStatus, connections: [String]) case chatInfoUpdated(user: UserRef, chatInfo: ChatInfo) case newChatItems(user: UserRef, chatItems: [AChatItem]) case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) @@ -1137,8 +1130,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case .receivedContactRequest: "receivedContactRequest" case .contactUpdated: "contactUpdated" case .groupMemberUpdated: "groupMemberUpdated" - case .networkStatus: "networkStatus" - case .networkStatuses: "networkStatuses" + case .subscriptionStatus: "subscriptionStatus" case .chatInfoUpdated: "chatInfoUpdated" case .newChatItems: "newChatItems" case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" @@ -1210,8 +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 .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + 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") @@ -1371,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) @@ -1410,11 +1369,6 @@ enum ForwardConfirmation: Decodable, Hashable { case filesFailed(filesCount: Int) } -struct ConnNetworkStatus: Decodable { - var agentConnId: String - var networkStatus: NetworkStatus -} - struct UserMsgReceiptSettings: Codable { var enable: Bool var clearOverrides: Bool diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index e5fd6362a3..5ebab167fd 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -283,29 +283,6 @@ class ChatTagsModel: ObservableObject { } } -class NetworkModel: ObservableObject { - // map of connections network statuses, key is agent connection id - @Published var networkStatuses: Dictionary = [:] - - static let shared = NetworkModel() - - private init() { } - - func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] = status - } - } - - func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] ?? .unknown - } else { - .unknown - } - } -} - /// ChatItemWithMenu can depend on previous or next item for it's appearance /// This dummy model is used to force an update of all chat items, /// when they might have changed appearance. @@ -374,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 c7e382f3ed..222d6021d9 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -15,8 +15,6 @@ import SwiftUI private var chatController: chat_ctrl? -private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock") - enum TerminalItem: Identifiable { case cmd(Date, ChatCommand) case res(Date, ChatAPIResult) @@ -1319,6 +1317,10 @@ func apiCreateUserAddress() async throws -> CreatedConnLink? { let userId = try currentUserId("apiCreateUserAddress") let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateMyAddress(userId: userId)) if case let .result(.userContactLinkCreated(_, connLink)) = r { return connLink } + if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r { + showClientNotice(server, preset, expires) + return nil + } if let r { throw r.unexpected } else { return nil } } @@ -1640,7 +1642,6 @@ func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: } else { ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat) } - NetworkModel.shared.setContactNetworkStatus(contact, .connected) inProgress?.wrappedValue = false } if contact.sndReady { @@ -1728,12 +1729,6 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws { } } -func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { - let r: ChatResponse1 = try chatSendCmdSync(.apiGetNetworkStatuses) - if case let .networkStatuses(_, statuses) = r { return statuses } - throw r.unexpected -} - func markChatRead(_ im: ItemsModel, _ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { @@ -1899,6 +1894,10 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } + if case let .error(.errorAgent(.NOTICE(server, preset, expires))) = r { + showClientNotice(server, preset, expires) + return nil + } if let r { throw r.unexpected } else { return nil } } @@ -1955,7 +1954,6 @@ func acceptMemberContact(contactId: Int64, inProgress: Binding? = nil) asy if let contact = await apiAcceptMemberContact(contactId: contactId) { await MainActor.run { ChatModel.shared.updateContact(contact) - NetworkModel.shared.setContactNetworkStatus(contact, .connected) inProgress?.wrappedValue = false } if contact.sndReady { @@ -2241,7 +2239,6 @@ class ChatReceiver { func processReceivedMsg(_ res: ChatEvent) async { let m = ChatModel.shared - let n = NetworkModel.shared logger.debug("processReceivedMsg: \(res.responseType)") switch res { case let .contactDeletedByContact(user, contact): @@ -2258,14 +2255,15 @@ func processReceivedMsg(_ res: ChatEvent) async { m.dismissConnReqView(conn.id) m.removeChat(conn.id) } + if contact.id == m.chatId, let conn = contact.activeConn { + m.chatAgentConnId = conn.agentConnId + m.chatSubStatus = .active + } } } if contact.directOrUsed { NtfManager.shared.notifyContactConnected(user, contact) } - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } case let .contactConnecting(user, contact): if active(user) && contact.directOrUsed { await MainActor.run { @@ -2286,9 +2284,6 @@ func processReceivedMsg(_ res: ChatEvent) async { } } } - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } case let .receivedContactRequest(user, contactRequest, chat_): if active(user) { await MainActor.run { @@ -2327,30 +2322,10 @@ func processReceivedMsg(_ res: ChatEvent) async { _ = m.upsertGroupMember(groupInfo, toMember) } } - case let .networkStatus(status, connections): - // dispatch queue to synchronize access - networkStatusesLock.sync { - var ns = n.networkStatuses - // slow loop is on the background thread - for cId in connections { - ns[cId] = status - } - // fast model update is on the main thread - DispatchQueue.main.sync { - n.networkStatuses = ns - } - } - case let .networkStatuses(_, statuses): () - // dispatch queue to synchronize access - networkStatusesLock.sync { - var ns = n.networkStatuses - // slow loop is on the background thread - for s in statuses { - ns[s.agentConnId] = s.networkStatus - } - // fast model update is on the main thread - DispatchQueue.main.sync { - n.networkStatuses = ns + case let .subscriptionStatus(status, connections): + if let chatAgentConnId = m.chatAgentConnId, connections.contains(chatAgentConnId) { + await MainActor.run { + m.chatSubStatus = status } } case let .chatInfoUpdated(user, chatInfo): @@ -2554,11 +2529,6 @@ func processReceivedMsg(_ res: ChatEvent) async { _ = m.upsertGroupMember(groupInfo, member) } } - if let contact = memberContact { - await MainActor.run { - n.setContactNetworkStatus(contact, .connected) - } - } case let .groupUpdated(user, toGroup): if active(user) { await MainActor.run { @@ -2784,13 +2754,10 @@ func processReceivedMsg(_ res: ChatEvent) async { func switchToLocalSession() { let m = ChatModel.shared - let n = NetworkModel.shared m.remoteCtrlSession = nil do { m.users = try listUsers() try getUserChatData() - let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) } - n.networkStatuses = Dictionary(uniqueKeysWithValues: statuses) } catch let error { logger.debug("error updating chat data: \(responseError(error))") } @@ -2898,3 +2865,26 @@ private struct UserResponse: Decodable { var user: User? var error: String? } + +private func showClientNotice(_ server: String, _ preset: Bool, _ expiresAt: Date?) { + DispatchQueue.main.async { + var message = "Server: \(server).\nConditions of use violation notice received from \(preset ? "preset" : "this") server.\nNo IDs shared, see How it works." + if let expiresAt { + message += "\n\nNew addresses can be created after \(expiresAt.formatted(date: .abbreviated, time: .shortened))." + } + showAlert("Not allowed", message: message) { + let howItWorks = UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in + UIApplication.shared.open(contentModerationPostLink) + }) + return preset + ? [ + okAlertAction, + UIAlertAction(title: NSLocalizedString("Conditions of use", comment: "alert button"), style: .default, handler: { _ in + UIApplication.shared.open(conditionsURL) + }), + howItWorks + ] + : [okAlertAction, howItWorks] + } + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index b60842a4a0..37f3b982a1 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var theme: AppTheme + @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat var imageSize: CGFloat = 32 @@ -56,6 +57,13 @@ struct ChatInfoToolbar: View { .padding(.top, -2) } } + if let contact = chat.chatInfo.contact, + contact.ready && contact.active, + let chatSubStatus = m.chatSubStatus, + chatSubStatus != .active { + SubStatusView(status: chatSubStatus) + .padding(.leading, 4) + } } .foregroundColor(theme.colors.onBackground) .frame(width: 220) @@ -68,6 +76,30 @@ struct ChatInfoToolbar: View { .baselineOffset(1) .kerning(-2) } + + struct SubStatusView: View { + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @EnvironmentObject var theme: AppTheme + var status: SubscriptionStatus + + var body: some View { + switch status { + case .active: EmptyView() + case .pending: ProgressView() + case .removed: subStatusError() + case .noSub: subStatusError() + } + } + + @ViewBuilder private func subStatusError() -> some View { + let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize + Image(systemName: "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .foregroundColor(theme.colors.secondary) + } + } } struct ChatInfoToolbar_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 77c1db341a..ad82af05e2 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -92,7 +92,6 @@ struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction - @ObservedObject var networkModel = NetworkModel.shared @ObservedObject var chat: Chat @State var contact: Contact @State var localAlias: String @@ -115,7 +114,7 @@ struct ChatInfoView: View { enum ChatInfoViewAlert: Identifiable { case clearChatAlert - case networkStatusAlert + case subStatusAlert(status: SubscriptionStatus) case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert @@ -126,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" @@ -236,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(status: chatSubStatus) + .onTapGesture { + alert = .subStatusAlert(status: chatSubStatus) + } + } if let connStats = connectionStats { Button("Change receiving address") { alert = .switchAddressAlert @@ -325,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: @@ -546,26 +547,6 @@ struct ChatInfoView: View { } } - private func networkStatusRow() -> some View { - HStack { - Text("Network status") - Image(systemName: "info.circle") - .foregroundColor(theme.colors.primary) - .font(.system(size: 14)) - Spacer() - Text(networkModel.contactNetworkStatus(contact).statusString) - .foregroundColor(theme.colors.secondary) - serverImage() - } - } - - private func serverImage() -> some View { - let status = networkModel.contactNetworkStatus(contact) - return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : theme.colors.secondary) - .font(.system(size: 12)) - } - private func deleteContactButton() -> some View { Button(role: .destructive) { deleteContactDialog( @@ -605,10 +586,10 @@ struct ChatInfoView: View { ) } - private func networkStatusAlert() -> Alert { + private func subStatusAlert(_ status: SubscriptionStatus) -> Alert { Alert( title: Text("Network status"), - message: Text(networkModel.contactNetworkStatus(contact).statusExplanation) + message: Text(status.statusExplanation) ) } @@ -667,6 +648,30 @@ struct ChatInfoView: View { } } +struct SubStatusRow: View { + @EnvironmentObject var theme: AppTheme + var status: SubscriptionStatus + + var body: some View { + HStack { + Text("Network status") + Image(systemName: "info.circle") + .foregroundColor(theme.colors.primary) + .font(.system(size: 14)) + Spacer() + Text(status.statusString) + .foregroundColor(theme.colors.secondary) + serverImage(status) + } + } + + private func serverImage(_ status: SubscriptionStatus) -> some View { + return Image(systemName: status.imageName) + .foregroundColor(status == .active ? .green : theme.colors.secondary) + .font(.system(size: 12)) + } +} + struct ChatTTLOption: View { @ObservedObject var chat: Chat @Binding var progressIndicator: Bool diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index fa53045391..709758655f 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 conn = contact.activeConn { + chatModel.chatAgentConnId = conn.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/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 683dea0f56..3745d0f0b8 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -788,7 +788,6 @@ struct ComposeView: View { await MainActor.run { self.chatModel.updateContact(contact) clearState() - NetworkModel.shared.setContactNetworkStatus(contact, .connected) } } else { AlertManager.shared.showAlertMsg(title: "Empty message!") @@ -827,7 +826,6 @@ struct ComposeView: View { if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognito, msg: mc) { await MainActor.run { self.chatModel.updateContact(contact) - NetworkModel.shared.setContactNetworkStatus(contact, .connected) clearState() } } else { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 2298af614e..01a3805910 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -160,7 +160,15 @@ struct GroupMemberInfoView: View { if let connStats = connectionStats { Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { - // TODO network connection status + if let subStatus = connStats.subStatus { + SubStatusRow(status: subStatus) + .onTapGesture { + showAlert( + NSLocalizedString("Network status", comment: "alert title"), + message: subStatus.statusExplanation + ) + } + } Button("Change receiving address") { alert = .switchAddressAlert } @@ -396,7 +404,6 @@ struct GroupMemberInfoView: View { ItemsModel.shared.loadOpenChat(memberContact.id) { dismissAllSheets(animated: true) } - NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index c56d947a5a..be2c456802 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -460,12 +460,6 @@ struct ChatPreviewView: View { @ViewBuilder private func chatStatusImage() -> some View { let size = dynamicSize(userFont).incognitoSize switch chat.chatInfo { - case let .direct(contact): - if contact.active, let status = contact.activeConn?.connStatus, status == .ready || status == .sndReady { - NetworkStatusView(contact: contact, size: size) - } else { - incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) - } case .group: if progressByTimeout { ProgressView() @@ -482,30 +476,6 @@ struct ChatPreviewView: View { incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } } - - struct NetworkStatusView: View { - @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize - @EnvironmentObject var theme: AppTheme - @ObservedObject var networkModel = NetworkModel.shared - - let contact: Contact - let size: CGFloat - - var body: some View { - let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize - switch (networkModel.contactNetworkStatus(contact)) { - case .connected: incognitoIcon(contact.contactConnIncognito, theme.colors.secondary, size: size) - case .error: - Image(systemName: "exclamationmark.circle") - .resizable() - .scaledToFit() - .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) - .foregroundColor(theme.colors.secondary) - default: - ProgressView() - } - } - } } @ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color, size: CGFloat) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 1e5b4bff16..d40ec116f4 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -196,12 +196,13 @@ struct UserAddressView: View { progressIndicator = true Task { do { - if let connLinkContact = try await apiCreateUserAddress() { - DispatchQueue.main.async { + let connLinkContact = try await apiCreateUserAddress() + DispatchQueue.main.async { + if let connLinkContact { chatModel.userAddress = UserContactLink(connLinkContact) alert = .shareOnCreate - progressIndicator = false } + progressIndicator = false } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 57788d4232..e23c535ebb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.6.0-1uYs2FKSnCtGti4vmlXmvH.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.2-6qgCrd2M1igHbyytG3wCNC.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 80cc1577c6..fce0f100f2 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -545,6 +545,7 @@ public struct ConnectionStats: Decodable, Hashable { 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) @@ -559,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" } @@ -606,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: String { + switch self { + case .active: NSLocalizedString("You are connected to the server used to receive messages from this connection.", comment: "subscription status explanation") + case .pending: NSLocalizedString("Trying to connect to the server used to receive messages from this connection.", comment: "subscription status explanation") + case let .removed(err): String.localizedStringWithFormat(NSLocalizedString("Error connecting to the server used to receive messages from this connection: %@", comment: "subscription status explanation"), err) + case .noSub: NSLocalizedString("You are not connected to the server used to receive messages from this connection (no subscription).", comment: "subscription status explanation") + } + } + + 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 } @@ -833,6 +879,7 @@ public enum AgentErrorType: Decodable, Hashable { case RCP(rcpErr: RCErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) + case NOTICE(server: String, preset: Bool, expiresAt: Date?) case INTERNAL(internalErr: String) case CRITICAL(offerRestart: Bool, criticalErr: String) case INACTIVE diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index aca4ec4a76..debd0ec328 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -96,11 +96,12 @@ object ChatModel { val dbMigrationInProgress = mutableStateOf(false) val incompleteInitializedDbRemoved = mutableStateOf(false) // map of connections network statuses, key is agent connection id - val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf(null) + val chatAgentConnId = mutableStateOf(null) + val chatSubStatus = mutableStateOf(null) val openAroundItemId: MutableState = mutableStateOf(null) val chatsContext = ChatsContext(null) val secondaryChatsContext = mutableStateOf(null) @@ -1147,21 +1148,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { - val conn = contact.activeConn - if (conn != null) { - networkStatuses[conn.agentConnId] = status - } - } - - fun contactNetworkStatus(contact: Contact): NetworkStatus { - val conn = contact.activeConn - return if (conn != null) - networkStatuses[conn.agentConnId] ?: NetworkStatus.Unknown() - else - NetworkStatus.Unknown() - } - fun addTerminalItem(item: TerminalItem) { val maxItems = if (appPreferences.developerTools.get()) 500 else 200 if (terminalsVisible.isNotEmpty()) { @@ -1721,30 +1707,6 @@ sealed class ChatInfo: SomeChat, NamedChat { } } -@Serializable -sealed class NetworkStatus { - val statusString: String get() = - when (this) { - is Connected -> generalGetString(MR.strings.server_connected) - is Error -> generalGetString(MR.strings.server_error) - else -> generalGetString(MR.strings.server_connecting) - } - val statusExplanation: String get() = - when (this) { - is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact) - is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError) - else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages) - } - - @Serializable @SerialName("unknown") class Unknown: NetworkStatus() - @Serializable @SerialName("connected") class Connected: NetworkStatus() - @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() - @Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus() -} - -@Serializable -data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus) - @Serializable data class Contact( val contactId: Long, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index a4f9aa1a50..5f024b8134 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -27,12 +28,14 @@ import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.item.contentModerationPostLink import chat.simplex.common.views.chat.item.showContentBlockedAlert import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.defaultConditionsLink import chat.simplex.common.views.usersettings.networkAndServers.serverHostname import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration @@ -46,12 +49,17 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle import java.util.Date typealias ChatCtrl = Long @@ -1709,6 +1717,11 @@ object ChatController { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } val r = sendCmdWithRetry(rh, CC.ApiCreateMyAddress(userId)) if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) { + val e = r.err.agentError + showClientNoticeAlert(e.server, e.preset, e.expiresAt) + return null + } if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) @@ -1842,13 +1855,6 @@ object ChatController { return r.result is CR.CmdOk } - suspend fun apiGetNetworkStatuses(rh: Long?): List? { - val r = sendCmd(rh, CC.ApiGetNetworkStatuses()) - if (r is API.Result && r.res is CR.NetworkStatuses) return r.res.networkStatuses - Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}") - return null - } - suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { val r = sendCmd(rh, CC.ApiChatRead(type, id, scope = null)) if (r.result is CR.CmdOk) return true @@ -2175,6 +2181,11 @@ object ChatController { suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink + if (r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.NOTICE) { + val e = r.err.agentError + showClientNoticeAlert(e.server, e.preset, e.expiresAt) + return null + } if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) @@ -2546,12 +2557,15 @@ object ChatController { chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") chatModel.chatsContext.removeChat(rhId, conn.id) } + if (r.contact.id == chatModel.chatId.value && conn != null) { + chatModel.chatAgentConnId.value = conn.agentConnId + chatModel.chatSubStatus.value = SubscriptionStatus.Active + } } } if (r.contact.directOrUsed) { ntfManager.notifyContactConnected(r.user, r.contact) } - chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { @@ -2576,7 +2590,6 @@ object ChatController { } } } - chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) } is CR.ReceivedContactRequest -> { val contactRequest = r.contactRequest @@ -2618,18 +2631,12 @@ object ChatController { } } } - // ContactsSubscribed, ContactsDisconnected are only used in CLI, - // They have to be used here for remote desktop to process these status updates. - is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) - is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) - is CR.NetworkStatusResp -> { - for (cId in r.connections) { - chatModel.networkStatuses[cId] = r.networkStatus - } - } - is CR.NetworkStatuses -> { - for (s in r.networkStatuses) { - chatModel.networkStatuses[s.agentConnId] = s.networkStatus + is CR.SubscriptionStatusEvt -> { + val chatAgentConnId = chatModel.chatAgentConnId.value + if (chatAgentConnId != null && r.connections.contains(chatAgentConnId)) { + withContext(Dispatchers.Main) { + chatModel.chatSubStatus.value = r.subscriptionStatus + } } } is CR.ChatInfoUpdated -> @@ -2916,9 +2923,6 @@ object ChatController { chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } } - if (r.memberContact != null) { - chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected()) - } } is CR.GroupUpdated -> if (active(r.user)) { @@ -3231,12 +3235,6 @@ object ChatController { m.users.clear() m.users.addAll(users) getUserChatData(null) - val statuses = apiGetNetworkStatuses(null) - if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) - } } private fun activeUser(rhId: Long?, user: UserLike): Boolean = @@ -3349,12 +3347,6 @@ object ChatController { } } - private fun updateContactsStatus(contactRefs: List, status: NetworkStatus) { - for (c in contactRefs) { - chatModel.networkStatuses[c.agentConnId] = status - } - } - suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded { // TODO lock the switch so that two switches can't run concurrently? chatModel.chatId.value = null @@ -3381,12 +3373,6 @@ object ChatController { chatModel.secondaryChatsContext.value?.popChatCollector?.clear() } } - val statuses = apiGetNetworkStatuses(rhId) - if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) - } getUserChatData(rhId) } @@ -3645,7 +3631,6 @@ sealed class CC { class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC() class ApiEndCall(val contact: Contact): CC() class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC() - class ApiGetNetworkStatuses(): CC() class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() @@ -3844,7 +3829,6 @@ sealed class CC { is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}" is ApiEndCall -> "/_call end @${contact.apiId}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" - is ApiGetNetworkStatuses -> "/_network_statuses" is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}" is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}" @@ -4019,7 +4003,6 @@ sealed class CC { is ApiSendCallExtraInfo -> "apiSendCallExtraInfo" is ApiEndCall -> "apiEndCall" is ApiCallStatus -> "apiCallStatus" - is ApiGetNetworkStatuses -> "apiGetNetworkStatuses" is ApiChatRead -> "apiChatRead" is ApiChatItemsRead -> "apiChatItemsRead" is ApiChatUnread -> "apiChatUnread" @@ -6159,12 +6142,7 @@ sealed class CR { @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR() @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR() @Serializable @SerialName("groupMemberUpdated") class GroupMemberUpdated(val user: UserRef, val groupInfo: GroupInfo, val fromMember: GroupMember, val toMember: GroupMember): CR() - // TODO remove below - @Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List): CR() - @Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List): CR() - // TODO remove above - @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List): CR() - @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List): CR() + @Serializable @SerialName("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List): CR() @Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): CR() @@ -6345,10 +6323,7 @@ sealed class CR { is ContactRequestRejected -> "contactRequestRejected" is ContactUpdated -> "contactUpdated" is GroupMemberUpdated -> "groupMemberUpdated" - is ContactsSubscribed -> "contactsSubscribed" - is ContactsDisconnected -> "contactsDisconnected" - is NetworkStatusResp -> "networkStatus" - is NetworkStatuses -> "networkStatuses" + is SubscriptionStatusEvt -> "subscriptionStatus" is ChatInfoUpdated -> "chatInfoUpdated" is NewChatItems -> "newChatItems" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" @@ -6521,10 +6496,7 @@ sealed class CR { is ContactRequestRejected -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}") is ContactUpdated -> withUser(user, json.encodeToString(toContact)) is GroupMemberUpdated -> withUser(user, "groupInfo: $groupInfo\nfromMember: $fromMember\ntoMember: $toMember") - is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" - is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" - is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections" - is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses)) + is SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections" is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo)) is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) @@ -6747,7 +6719,8 @@ class ConnectionStats( val rcvQueuesInfo: List, val sndQueuesInfo: List, val ratchetSyncState: RatchetSyncState, - val ratchetSyncSupported: Boolean + val ratchetSyncSupported: Boolean, + var subStatus: SubscriptionStatus? ) { val ratchetSyncAllowed: Boolean get() = ratchetSyncSupported && listOf(RatchetSyncState.Allowed, RatchetSyncState.Required).contains(ratchetSyncState) @@ -6762,8 +6735,10 @@ class ConnectionStats( @Serializable class RcvQueueInfo( val rcvServer: String, + var status: QueueStatus, val rcvSwitchStatus: RcvSwitchStatus?, - var canAbortSwitch: Boolean + var canAbortSwitch: Boolean, + var subStatus: SubscriptionStatus ) @Serializable @@ -6777,6 +6752,7 @@ enum class RcvSwitchStatus { @Serializable class SndQueueInfo( val sndServer: String, + var status: QueueStatus, val sndSwitchStatus: SndSwitchStatus? ) @@ -6814,6 +6790,39 @@ enum class RatchetSyncState { @SerialName("agreed") Agreed } +@Serializable +enum class QueueStatus { + @SerialName("new") New, + @SerialName("confirmed") Confirmed, + @SerialName("secured") Secured, + @SerialName("active") Active, + @SerialName("disabled") Disabled +} + +@Serializable +sealed class SubscriptionStatus { + @Serializable @SerialName("active") object Active: SubscriptionStatus() + @Serializable @SerialName("pending") object Pending: SubscriptionStatus() + @Serializable @SerialName("removed") class Removed(val subError: String): SubscriptionStatus() + @Serializable @SerialName("noSub") object NoSub: SubscriptionStatus() + + val statusString: String get() = + when (this) { + is Active -> generalGetString(MR.strings.server_connected) + is Pending -> generalGetString(MR.strings.server_connecting) + is Removed -> generalGetString(MR.strings.server_error) + is NoSub -> generalGetString(MR.strings.server_no_sub) + } + + val statusExplanation: String get() = + when (this) { + is Active -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact) + is Pending -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages) + is Removed -> String.format(generalGetString(MR.strings.error_connecting_to_server_to_receive_messages), subError) + is NoSub -> generalGetString(MR.strings.not_connected_to_server_to_receive_messages_no_sub) + } +} + interface SimplexAddress { val connLinkContact: CreatedConnLink val shortLinkDataSet: Boolean @@ -7306,6 +7315,7 @@ sealed class AgentErrorType { is RCP -> "RCP ${rcpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" + is NOTICE -> "NOTICE $server $expiresAt" is INTERNAL -> "INTERNAL $internalErr" is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" @@ -7319,6 +7329,7 @@ sealed class AgentErrorType { @Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() + @Serializable @SerialName("NOTICE") class NOTICE(val server: String, val preset: Boolean, val expiresAt: Instant?): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() @@ -8046,3 +8057,34 @@ enum class MsgType { @SerialName("quota") QUOTA } + +fun showClientNoticeAlert(server: String, preset: Boolean, expiresAt: Instant?) { + var message = "Server: $server.\nConditions of use violation notice received from ${if (preset) "preset" else "this"} server.\nNo ID shared, see How it works." + if (expiresAt != null) { + val tz = TimeZone.currentSystemDefault() + val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) + message += "\n\nNew addresses can be created after ${expiresAt.toLocalDateTime(tz).toJavaLocalDateTime().format(formatter)}." + } + AlertManager.shared.showAlertDialogButtonsColumn(title = "Not allowed", text = AnnotatedString(message)) { + val uriHandler = LocalUriHandler.current + Column { + SectionItemView({ AlertManager.shared.hideAlert() }) { + Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (preset) { + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(defaultConditionsLink) + }) { + Text(generalGetString(MR.strings.operator_conditions_of_use), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(contentModerationPostLink) + }) { + Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index c7b8cb2c81..061ea71016 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -72,9 +72,6 @@ fun ChatInfoView( val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { - mutableStateOf(chatModel.contactNetworkStatus(contact)) - } val chatRh = chat.remoteHostId val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) } @@ -101,7 +98,6 @@ fun ChatInfoView( setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) }, connStats = connStats, - contactNetworkStatus.value, customUserProfile, localAlias, connectionCode, @@ -524,7 +520,6 @@ fun ChatInfoLayout( chatItemTTL: MutableState, setChatItemTTL: (ChatItemTTL?) -> Unit, connStats: MutableState, - contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, connectionCode: String?, @@ -643,13 +638,16 @@ fun ChatInfoLayout( if (contact.ready && contact.active) { SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { - SectionItemView({ - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.network_status), - contactNetworkStatus.statusExplanation - ) - }) { - NetworkStatusRow(contactNetworkStatus) + val chatSubStatus = chatModel.chatSubStatus.value + if (chatSubStatus != null) { + SectionItemView({ + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.network_status), + chatSubStatus.statusExplanation + ) + }) { + SubStatusRow(chatSubStatus) + } } if (cStats != null) { SwitchAddressButton( @@ -1063,7 +1061,7 @@ fun InfoViewActionButton( } @Composable -private fun NetworkStatusRow(networkStatus: NetworkStatus) { +fun SubStatusRow(subStatus: SubscriptionStatus) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -1086,25 +1084,24 @@ private fun NetworkStatusRow(networkStatus: NetworkStatus) { horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - networkStatus.statusString, + subStatus.statusString, color = MaterialTheme.colors.secondary ) - ServerImage(networkStatus) + ServerImage(subStatus) } } } @Composable -private fun ServerImage(networkStatus: NetworkStatus) { +private fun ServerImage(subStatus: SubscriptionStatus) { Box(Modifier.size(18.dp)) { - when (networkStatus) { - is NetworkStatus.Connected -> + when (subStatus) { + is SubscriptionStatus.Active -> Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = Color.Green) - is NetworkStatus.Disconnected -> + is SubscriptionStatus.Pending -> Icon(painterResource(MR.images.ic_pending_filled), stringResource(MR.strings.icon_descr_server_status_disconnected), tint = MaterialTheme.colors.secondary) - is NetworkStatus.Error -> + is SubscriptionStatus.Removed, SubscriptionStatus.NoSub -> Icon(painterResource(MR.images.ic_error_filled), stringResource(MR.strings.icon_descr_server_status_error), tint = MaterialTheme.colors.secondary) - else -> Icon(painterResource(MR.images.ic_circle), stringResource(MR.strings.icon_descr_server_status_pending), tint = MaterialTheme.colors.secondary) } } } @@ -1455,7 +1452,6 @@ fun PreviewChatInfoLayout() { connectionCode = "123", developerTools = false, connStats = remember { mutableStateOf(null) }, - contactNetworkStatus = NetworkStatus.Connected(), onLocalAliasChanged = {}, customUserProfile = null, openPreferences = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index c7bc2e2724..26daee363f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -102,6 +102,7 @@ fun ChatView( val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } + val chatRh = remoteHostId.value val activeChat = remember { derivedStateOf { var chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } val chatInfo = chat?.chatInfo @@ -120,6 +121,8 @@ fun ChatView( if (chat == null || chatInfo == null || user == null) { LaunchedEffect(Unit) { chatModel.chatId.value = null + chatModel.chatAgentConnId.value = null + chatModel.chatSubStatus.value = null ModalManager.end.closeModals() } } else { @@ -168,6 +171,25 @@ fun ChatView( showSearch.value = false searchText.value = "" selectedChatItems.value = null + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) { + withBGApi { + val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + if (r != null) { + val contactStats = r.first + if (contactStats != null) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, chat.chatInfo.contact, contactStats) + chatModel.chatAgentConnId.value = chat.chatInfo.contact.activeConn.agentConnId + chatModel.chatSubStatus.value = contactStats.subStatus + } + } + } + } else { + withContext(Dispatchers.Main) { + chatModel.chatAgentConnId.value = null + chatModel.chatSubStatus.value = null + } + } } } } @@ -184,7 +206,6 @@ fun ChatView( } } val view = LocalMultiplatformView() - val chatRh = remoteHostId.value // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { @@ -356,7 +377,7 @@ fun ChatView( if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + KeyChangeEffect(chatInfo.id) { contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedContactInfo = contactInfo code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -1281,9 +1302,51 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo ) } } + val chatSubStatus = chatModel.chatSubStatus.value + if ( + cInfo is ChatInfo.Direct && + cInfo.contact.ready && + cInfo.contact.active && + chatSubStatus != null && + chatSubStatus != SubscriptionStatus.Active + ) { + Box( + Modifier.padding(start = 10.dp) + ) { + SubStatusView(chatSubStatus) + } + } } } +@Composable +fun SubStatusView(status: SubscriptionStatus) { + when (status) { + SubscriptionStatus.Active -> + Box {} + SubscriptionStatus.Pending -> + SubProgressView() + is SubscriptionStatus.Removed, SubscriptionStatus.NoSub -> + Icon( + painterResource(MR.images.ic_error), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(19.sp.toDp()) + ) + } +} + +@Composable +private fun SubProgressView() { + CircularProgressIndicator( + Modifier + .size(15.sp.toDp()), + color = MaterialTheme.colors.secondary, + strokeWidth = 1.5.dp + ) +} + @Composable private fun SupportChatsCountToolbar( chatInfo: ChatInfo, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt index 5c8ac3bc5d..21799ff820 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt @@ -162,7 +162,6 @@ fun acceptMemberContact( chatModel.chatsContext.updateContact(rhId, contact) inProgress?.value = false } - chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) close?.invoke(chat) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index b01d07d9b8..af4baad90f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -533,7 +533,6 @@ fun ComposeView( withContext(Dispatchers.Main) { chatsCtx.updateContact(chat.remoteHostId, contact) clearState() - chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) } } else { composeState.value = composeState.value.copy(inProgress = false) @@ -554,7 +553,6 @@ fun ComposeView( withContext(Dispatchers.Main) { chatsCtx.updateContact(chat.remoteHostId, contact) clearState() - chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) } } else { composeState.value = composeState.value.copy(inProgress = false) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 7febf2a904..d7e052e502 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -109,7 +109,6 @@ fun GroupMemberInfoView( } openDirectChat(rhId, memberContact.contactId) closeAll() - chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } progressIndicator = false } @@ -495,6 +494,17 @@ fun GroupMemberInfoLayout( if (cStats != null) { SectionDividerSpaced() SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { + val subStatus = cStats.subStatus + if (subStatus != null) { + SectionItemView({ + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.network_status), + subStatus.statusExplanation + ) + }) { + SubStatusRow(subStatus) + } + } SwitchAddressButton( disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || !member.sendMsgEnabled, switchAddress = switchMemberAddress diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 4a8e9e5193..77ab62dbf1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -62,12 +62,11 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { when (chat.chatInfo) { is ChatInfo.Direct -> { - val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) val defaultClickAction = { if (chatModel.chatId.value != chat.id) scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } } ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { - ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, click = defaultClickAction, @@ -87,7 +86,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { - ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction) } }, click = defaultClickAction, @@ -107,7 +106,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { - ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, click = defaultClickAction, @@ -744,7 +743,6 @@ fun acceptContactRequest( } inProgress?.value = false } - chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) close?.invoke(chat) } else { inProgress?.value = false @@ -1057,7 +1055,6 @@ fun PreviewChatListNavLinkDirect() { null, null, null, - null, disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, @@ -1103,7 +1100,6 @@ fun PreviewChatListNavLinkGroup() { null, null, null, - null, disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index d5d6facafe..4280845867 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -42,7 +42,6 @@ fun ChatPreviewView( chatModelDraft: ComposeState?, chatModelDraftChatId: ChatId?, currentUserProfileDisplayName: String?, - contactNetworkStatus: NetworkStatus?, disabled: Boolean, linkMode: SimplexLinkMode, inProgress: Boolean, @@ -349,33 +348,7 @@ fun ChatPreviewView( @Composable fun chatStatusImage() { - if (cInfo is ChatInfo.Direct) { - if ( - cInfo.contact.active && - (cInfo.contact.activeConn?.connStatus == ConnStatus.Ready || cInfo.contact.activeConn?.connStatus == ConnStatus.SndReady) - ) { - val descr = contactNetworkStatus?.statusString - when (contactNetworkStatus) { - is NetworkStatus.Connected -> - IncognitoIcon(chat.chatInfo.incognito) - - is NetworkStatus.Error -> - Icon( - painterResource(MR.images.ic_error), - contentDescription = descr, - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(19.sp.toDp()) - .offset(x = 2.sp.toDp()) - ) - - else -> - progressView() - } - } else { - IncognitoIcon(chat.chatInfo.incognito) - } - } else if (cInfo is ChatInfo.Group) { + if (cInfo is ChatInfo.Group) { if (progressByTimeout) { progressView() } else if (chat.chatStats.reportsCount > 0) { @@ -636,6 +609,6 @@ private data class ActiveVoicePreview( @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {}) + ChatPreviewView(Chat.sampleData, true, null, null, "", disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {}) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 55ac9c8810..ed1c7116e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -64,7 +64,7 @@ enum class SubscriptionColorType { ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED } -data class SubscriptionStatus( +data class AppSubscriptionStatus( val color: SubscriptionColorType, val variableValue: Float, val statusPercent: Float @@ -75,7 +75,7 @@ fun subscriptionStatusColorAndPercentage( socksProxy: String?, subs: SMPServerSubs, hasSess: Boolean -): SubscriptionStatus { +): AppSubscriptionStatus { fun roundedToQuarter(n: Float): Float = when { n >= 1 -> 1f n <= 0 -> 0f @@ -83,25 +83,25 @@ fun subscriptionStatusColorAndPercentage( } val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE - val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f) + val noConnColorAndPercent = AppSubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f) val activeSubsRounded = roundedToQuarter(subs.shareOfActive) return if (!online) noConnColorAndPercent else if (subs.total == 0 && !hasSess) // On freshly installed app (without chats) and on app start - SubscriptionStatus(activeColor, 0f, 0f) + AppSubscriptionStatus(activeColor, 0f, 0f) else if (subs.ssActive == 0) { if (hasSess) - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) else noConnColorAndPercent } else { // ssActive > 0 if (hasSess) - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) else // This would mean implementation error - SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) + AppSubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index cc72387875..c619ae6ebc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -612,13 +612,14 @@ private fun SingleOperatorUsageConditionsView( } } +val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + @Composable fun ConditionsTextView( rhId: Long? ) { val conditionsData = remember { mutableStateOf?>(null) } val failedToLoad = remember { mutableStateOf(false) } - val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" val scope = rememberCoroutineScope() // can show conditions when animation between modals finishes to prevent glitches val canShowConditionsAt = remember { System.currentTimeMillis() + 300 } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 96c539109d..fc43a97cb5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1131,7 +1131,7 @@ محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه. اختيار ملف إرسال غير مصرح به - محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). + محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). تشغيل خوادم WebRTC ICE أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index b13e3f4046..8c8493358e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -32,9 +32,11 @@ connected error connecting - You are connected to the server used to receive messages from this contact. - Trying to connect to the server used to receive messages from this contact (error: %1$s). - Trying to connect to the server used to receive messages from this contact. + no subscription + You are connected to the server used to receive messages from this connection. + Trying to connect to the server used to receive messages from this connection. + Error connecting to the server used to receive messages from this connection: %1$s. + You are not connected to the server used to receive messages from this connection (no subscription). deleted diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index bf04fd6a1e..03a8bcfdc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -1182,7 +1182,7 @@ Тази настройка се прилага за съобщения в текущия ви профил За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. Опит за свързване със сървъра, използван за получаване на съобщения от този контакт. - Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s). + Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s). Тестът е неуспешен на стъпка %s. Базата данни не работи правилно. Докоснете, за да научите повече Изображението не може да бъде декодирано. Моля, опитайте с друго изображение или се свържете с разработчиците. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index c9b1f79694..81d8ab2d70 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -1556,7 +1556,7 @@ Comproveu que l\'enllaç SimpleX sigui correcte. l\'enviament de fitxers encara no està suportat Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte. - S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s). + S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s). Usar perfil actual Usar nou perfil incògnit Error aplicació diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 4d6aa47fd3..a7e0f20faf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -493,7 +493,7 @@ Připojtíte se ke všem členům skupiny. Připojení Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu. - Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s). + Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s). označeno jako smazáno Odesílání souborů zatím není podporováno přijímání souborů zatím není podporováno diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml index c4f84d4397..8b9bb95675 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml @@ -178,7 +178,7 @@ fejl forbinder Du har forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt. - Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt (fejl: %1$s). + Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt (fejl: %1$s). Forsøger at oprette forbindelse til den server, der bruges til at modtage beskeder fra denne kontakt. slettet markeret som slettet diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index db5e983983..439ef2e3c5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -14,7 +14,7 @@ Fehler Verbinde Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. - Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s). + Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s). Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird. Gelöscht diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index fa356715db..811ba5533e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -806,7 +806,7 @@ ¿Actualizar la configuración de red\? Intentando conectar con el servidor para recibir mensajes de este contacto. formato de mensaje desconocido - Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s). + Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s). Prueba no superada en el paso %s. Pulsa para iniciar chat nuevo Compartir mensaje… diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index cbb4849067..2625baa5b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -35,7 +35,7 @@ متصل شدن مسیر نامعتبر فایل برنامه از کار افتاد - در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). + در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). حذف شد علامت گذاشته شده به عنوان حذف شده توسط %s حذف شد diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 1dd6598ef3..c6b094ab56 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -1036,7 +1036,7 @@ Profiilisi lähetetään kontaktille, jolta sait tämän linkin. Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin. Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta. - Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s). + Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta (virhe: %1$s). sinä kertalinkillä %1$s:n kautta diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 46b6e6e2fd..563b1a02c7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -52,7 +52,7 @@ Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre. Erreur de connexion Erreur lors de l\'ajout de membre·s - Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s). + Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %1$s). format de message invalide Lien entier Erreur lors de la sauvegarde des serveurs SMP diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index a3c6a65e32..b75847d8ba 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -1317,7 +1317,7 @@ A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. Érvénytelen fájlelérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek. Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet. - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s). + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s). A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 55b108cfb9..13b08e42c5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -104,7 +104,7 @@ Tautan tidak valid Periksa apakah tautan SimpleX sudah benar. Anda terhubung ke server yang digunakan untuk menerima pesan dari kontak ini. - Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s). + Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s). Migrasi basis data sedang berlangsung, \nmemerlukan waktu beberapa menit. menghubungkan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 35b0bab541..eadd5672b2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -201,7 +201,7 @@ Ripristina OK Connettere via indirizzo del contatto? - Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s). + Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %1$s). Ti connetterai a tutti i membri del gruppo. connessione %1$d Descrizione diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index b71831c9e0..6b413c9bfa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -1063,7 +1063,7 @@ כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם. פרופילי צ׳אט לעדכן מצב בידוד תעבורה\? - מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s). + מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה (שגיאה: %1$s). פורמט הודעה לא ידוע דרך הדפדפן בטל השתקה diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index efc1810bac..cc85e49a8c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -887,7 +887,7 @@ あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。 あなたのチャットプロフィールが他のグループメンバーに公開されます。 エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 - このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。 + このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。 使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。 \n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。 接続を完了するには、連絡相手がオンラインになる必要があります。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index adf66650f1..1e71459df9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -1404,7 +1404,7 @@ Jūsų profilis bus išsiųstas kontaktui iš kurio gavote šią nuorodą. Prisijungsite prie visų grupės narių. Esate prisijungę prie serverio skirto gauti žinutes iš šio kontakto. - Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s). + Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s). Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto. nėra detalių SimpleX fono tarnybą - ji naudoja kelis procentus akumuliatoriaus per dieną.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index b5be8b9773..86821db7a5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -913,7 +913,7 @@ SimpleX links Eenmalige SimpleX uitnodiging Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen. - Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s). + Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s). onbekend berichtformaat Via browser via contact adres link diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index a7862ffcf1..0b59cc1b06 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -26,7 +26,7 @@ wysyłanie plików nie jest jeszcze obsługiwane odbieranie plików nie jest jeszcze obsługiwane Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu. - Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s). + Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %1$s). nieznany format wiadomości SimpleX Ty diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 285d6f3802..c0bbe4d6bb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -971,7 +971,7 @@ Toque para ativar o perfil. Mostrar perfil de chat Mostrar perfil - Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s). + Tentando se conectar ao servidor utilizado para receber mensagens deste contato (erro:%1$s). Tentando se conectar ao servidor utilizado para receber mensagens deste contato. Você está conectado ao servidor usado para receber mensagens desse contato. Seu servidor diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 7232cc56d3..f51fb74d2a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -2077,7 +2077,7 @@ Imaginea nu poate fi decodificată. Vă rugăm să încercați o altă imagine sau să contactați dezvoltatorii. Serverele pentru fișierele noi ale profilului tău de conversații actual Acest grup nu mai există. - Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s). + Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s). Actualizarea setărilor va reconecta clientul la toate serverele. Acest șir de caractere nu este un link! S-a atins timpul de expirare la conectarea la desktop diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 6bb357fea8..7d7030fa42 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -14,7 +14,7 @@ ошибка соединяется Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта. - Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s). + Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %1$s). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта. удалено diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index b4d854c3d1..411cbde4c4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -1043,7 +1043,7 @@ คุณได้รับเชิญให้เข้าร่วมกลุ่ม โปรไฟล์แบบสุ่มของคุณ ธีม - กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s) + กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %1$s) SimpleX คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ โปรไฟล์ของคุณจะถูกส่งไปยังผู้ติดต่อที่ส่งลิงก์นี้มาให้คุณ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index ec8af9053e..501f07ea50 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -1023,7 +1023,7 @@ Sohbet profillerini parola ile koru! Daha az pil kullanımı Gizliliği korumak için, SimpleX her bir konuşma için farklı bir ID kullanır. - Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s). + Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s). Alıcılar güncellemeleri siz yazdıkça görürler. Bilgilerinizi kullanarak giriş yapın Mesajlarda Markdown diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index ddd54b717f..d2c1505759 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -124,7 +124,7 @@ помилка підключення Ви підключені до сервера для отримання повідомлень від цього контакту. - Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s). + Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s). видалено Спроба підключитися до сервера для отримання повідомлень від цього контакту. відзначено як видалено diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index b71350ea50..ae22c40277 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -2034,7 +2034,7 @@ Bật Tổng không xác định - Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này (lỗi: %1$s). + Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này (lỗi: %1$s). Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này. Để xác minh mã hóa đầu cuối với liên hệ của bạn, so sánh (hoặc quét) mã trên các thiết bị của các bạn. Cách ly truyền tải diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index d6686f358d..2091e7d25b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -589,7 +589,7 @@ 已删除 %1$s 你删除了 %1$s 你的个人资料将发送给你收到此链接的联系人。 - 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 + 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 你已连接到用于接收该联系人消息的服务器。 你分享了一次性链接 很可能此联系人已经删除了与你的联系。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index ee0c785e9f..8a444d6c2b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -238,7 +238,7 @@ 錯誤 連接中 你已連接到此聯絡人使用的伺服器以接收訊息。 - 嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。 + 嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。 正在嘗試連接到用於接收此聯絡人訊息伺服器。 已刪除 已標記為已刪除 diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index d4a46e59ab..923fab14c2 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -47,6 +47,7 @@ This file is generated automatically. - [ChatType](#chattype) - [ChatWallpaper](#chatwallpaper) - [ChatWallpaperScale](#chatwallpaperscale) +- [ClientNotice](#clientnotice) - [Color](#color) - [CommandError](#commanderror) - [CommandErrorType](#commanderrortype) @@ -288,6 +289,12 @@ AGENT: - type: "AGENT" - agentErr: [SMPAgentError](#smpagenterror) +NOTICE: +- type: "NOTICE" +- server: string +- preset: bool +- expiresAt: UTCTime? + INTERNAL: - type: "INTERNAL" - internalErr: string @@ -315,6 +322,7 @@ INACTIVE: **Record type**: - reason: [BlockingReason](#blockingreason) +- notice: [ClientNotice](#clientnotice)? --- @@ -1378,6 +1386,14 @@ self == 'direct' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' // J - "repeat" +--- + +## ClientNotice + +**Record type**: +- ttl: int64? + + --- ## Color diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index b673937d63..4cce44e588 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -349,7 +349,6 @@ undocumentedCommands = "APIGetContactCode", "APIGetGroupMemberCode", "APIGetNetworkConfig", - "APIGetNetworkStatuses", "APIGetNtfConns", "APIGetNtfToken", "APIGetReactionMembers", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index 1dbf484579..4a49551a54 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -180,7 +180,7 @@ undocumentedEvents = "CEvtGroupMemberSwitch", "CEvtHostConnected", "CEvtHostDisconnected", - "CEvtNetworkStatus", + "CEvtSubscriptionStatus", "CEvtNewRemoteHost", "CEvtNoMemberContactCreating", "CEvtNtfMessage", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 3a08606ea3..154d44b6c2 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -167,7 +167,6 @@ undocumentedResponses = "CRMemberSupportChatDeleted", "CRMemberSupportChats", "CRNetworkConfig", - "CRNetworkStatuses", "CRNewMemberContact", "CRNewMemberContactSentInv", "CRMemberContactAccepted", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index c762600424..73e427fa03 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -42,6 +42,7 @@ import Simplex.Messaging.Client import Simplex.Messaging.Crypto.File import Simplex.Messaging.Parsers (dropPrefix, fstToLower) import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..)) +import Simplex.Messaging.Protocol.Types (ClientNotice (..)) import Simplex.Messaging.Transport import Simplex.RemoteControl.Types import System.Console.ANSI.Types (Color (..)) @@ -228,6 +229,7 @@ chatTypesDocsData = (sti @CIMentionMember, STRecord, "", [], "", ""), (sti @CIReactionCount, STRecord, "", [], "", ""), (sti @CITimed, STRecord, "", [], "", ""), + (sti @ClientNotice, STRecord, "", [], "", ""), (sti @Color, STEnum, "", [], "", ""), (sti @CommandError, STUnion, "", [], "", ""), (sti @CommandErrorType, STUnion, "", [], "", ""), @@ -413,6 +415,7 @@ deriving instance Generic CIMention deriving instance Generic CIMentionMember deriving instance Generic CIReactionCount deriving instance Generic CITimed +deriving instance Generic ClientNotice deriving instance Generic Color deriving instance Generic CommandError deriving instance Generic CommandErrorType diff --git a/cabal.project b/cabal.project index 254ca50d6f..eeadf7c6fd 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 1dbc15b2e6225c0e254564747bc8412970273e85 + tag: 1329fc726ffb2e773935ad10f024a137dd887867 source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 64eb1571b9..b8095affdf 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -65,6 +65,7 @@ export type AgentErrorType = | AgentErrorType.RCP | AgentErrorType.BROKER | AgentErrorType.AGENT + | AgentErrorType.NOTICE | AgentErrorType.INTERNAL | AgentErrorType.CRITICAL | AgentErrorType.INACTIVE @@ -82,6 +83,7 @@ export namespace AgentErrorType { | "RCP" | "BROKER" | "AGENT" + | "NOTICE" | "INTERNAL" | "CRITICAL" | "INACTIVE" @@ -152,6 +154,13 @@ export namespace AgentErrorType { agentErr: SMPAgentError } + export interface NOTICE extends Interface { + type: "NOTICE" + server: string + preset: boolean + expiresAt?: string // ISO-8601 timestamp + } + export interface INTERNAL extends Interface { type: "INTERNAL" internalErr: string @@ -174,6 +183,7 @@ export interface AutoAccept { export interface BlockingInfo { reason: BlockingReason + notice?: ClientNotice } export enum BlockingReason { @@ -1587,6 +1597,10 @@ export enum ChatWallpaperScale { Repeat = "repeat", } +export interface ClientNotice { + ttl?: number // int64 +} + export enum Color { Black = "black", Red = "red", diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6ef3ac65c4..1e3eda8f2b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."1dbc15b2e6225c0e254564747bc8412970273e85" = "03hmlynixssyp0720h984slw4lkrzn3kr63k3mah50lbyxzsmnrs"; + "https://github.com/simplex-chat/simplexmq.git"."1329fc726ffb2e773935ad10f024a137dd887867" = "0wlpwr464i8dif5a94mfx31y3fm44gkc3h357dx8l1ii9q3sy05i"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 74855355f7..e8f43f5f07 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -87,7 +87,6 @@ library Simplex.Chat.Types.Preferences Simplex.Chat.Types.Shared Simplex.Chat.Types.UITheme - Simplex.Chat.Types.Util Simplex.Chat.Util if !flag(client_library) exposed-modules: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index c64954498a..9b711c2b50 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -149,7 +149,6 @@ newChatController eventSeq <- newTVarIO 0 inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize - connNetworkStatuses <- TM.emptyIO subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO entityLocks <- TM.emptyIO @@ -191,7 +190,6 @@ newChatController eventSeq, inputQ, outputQ, - connNetworkStatuses, subscriptionMode, chatLock, entityLocks, @@ -252,7 +250,7 @@ newChatController ops <- getUpdateServerOperators db presetOps (null users) let opDomains = operatorDomains $ mapMaybe snd ops (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users - pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains} + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains, presetServers = L.toList allPresetServers} where optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] optServers srvs overrides_ = case L.nonEmpty overrides_ of diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 10c218b84e..8003f66324 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -231,7 +231,6 @@ data ChatController = ChatController eventSeq :: TVar Int, inputQ :: TBQueue String, outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent), - connNetworkStatuses :: TMap AgentConnId NetworkStatus, subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, entityLocks :: TMap ChatLockEntity Lock, @@ -354,7 +353,6 @@ data ChatCommand | APIEndCall ContactId | APIGetCallInvitations | APICallStatus ContactId WebRTCCallStatus - | APIGetNetworkStatuses | APIUpdateProfile {userId :: UserId, profile :: Profile} | APISetContactPrefs {contactId :: ContactId, preferences :: Preferences} | APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias} @@ -729,7 +727,6 @@ data ChatResponse | CRGroupAliasUpdated {user :: User, toGroup :: GroupInfo} | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} - | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMemberAccepted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMemberSupportChatRead {user :: User, groupInfo :: GroupInfo, member :: GroupMember} @@ -829,7 +826,7 @@ data ChatEvent | CEvtContactAnotherClient {user :: User, contact :: Contact} | CEvtConnectionsDiff {userIds :: DatabaseDiff AgentUserId, connIds :: DatabaseDiff AgentConnId} | CEvtSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} - | CEvtNetworkStatus {server :: SMPServer, networkStatus :: NetworkStatus, connections :: [AgentConnId]} + | CEvtSubscriptionStatus {server :: SMPServer, subscriptionStatus :: SubscriptionStatus, connections :: [AgentConnId]} | CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} @@ -909,7 +906,7 @@ allowRemoteEvent = \case logEventToFile :: ChatEvent -> Bool logEventToFile = \case - CEvtNetworkStatus {} -> True + CEvtSubscriptionStatus {} -> True CEvtHostConnected {} -> True CEvtHostDisconnected {} -> True CEvtConnectionDisabled {} -> True @@ -1472,10 +1469,6 @@ chatModifyVar' :: (ChatController -> TVar a) -> (a -> a) -> CM' () chatModifyVar' f newValue = asks f >>= atomically . (`modifyTVar'` newValue) {-# INLINE chatModifyVar' #-} -setContactNetworkStatus :: Contact -> NetworkStatus -> CM' () -setContactNetworkStatus Contact {activeConn = Nothing} _ = pure () -setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar' connNetworkStatuses $ M.insert agentConnId status - onChatError :: CM a -> CM b -> CM a a `onChatError` onErr = a `catchAllErrors` \e -> onErr >> throwError e {-# INLINE onChatError #-} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 3f7738656e..67953897cf 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1368,8 +1368,6 @@ processChatCommand vr nm = \case user <- getUserByContactId db contactId contact <- getContact db vr user contactId pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs} - APIGetNetworkStatuses -> withUser $ \_ -> - CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses APICallStatus contactId receivedStatus -> withCurrentCall contactId $ \user ct call -> updateCallItemStatus user ct call receivedStatus Nothing $> Just call @@ -1777,7 +1775,7 @@ processChatCommand vr nm = \case subMode <- chatReadVar subscriptionMode let userData = contactShortLinkData (userProfileDirect user incognitoProfile Nothing True) Nothing -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation (Just userData) Nothing IKPQOn subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -1819,7 +1817,7 @@ processChatCommand vr nm = \case | short = Just $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing | otherwise = Nothing -- TODO [certs rcv] - (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True SCMInvitation userData_ Nothing IKPQOn subMode + (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userData_ Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -2011,7 +2009,7 @@ processChatCommand vr nm = \case subMode <- chatReadVar subscriptionMode let userData = contactShortLinkData (userProfileDirect user Nothing Nothing True) Nothing -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) Nothing IKPQOn subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode pure $ CRUserContactLinkCreated user ccLink' @@ -2231,7 +2229,7 @@ processChatCommand vr nm = \case gVar <- asks random subMode <- chatReadVar subscriptionMode -- TODO [certs rcv] - (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2635,7 +2633,7 @@ processChatCommand vr nm = \case let userData = encodeShortLinkData $ GroupShortLinkData groupProfile crClientData = encodeJSON $ CRDataGroup groupLinkId -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) (Just crClientData) IKPQOff subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userData) (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode @@ -2676,12 +2674,11 @@ processChatCommand vr nm = \case subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? -- TODO [certs rcv] - (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) -- TODO not sure it is correct to set connections status here? - lift $ setContactNetworkStatus ct NSConnected pure $ CRNewMemberContact user ct g m _ -> throwChatError CEGroupMemberNotActive APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do @@ -4392,7 +4389,6 @@ chatCommandP = "/_call end @" *> (APIEndCall <$> A.decimal), "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), "/_call get" $> APIGetCallInvitations, - "/_network_statuses" $> APIGetNetworkStatuses, "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 06f71fcaca..75eb208962 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -131,19 +131,16 @@ processAgentMessageNoConn :: AEvent 'AENone -> CM () processAgentMessageNoConn = \case CONNECT p h -> hostEvent $ CEvtHostConnected p h DISCONNECT p h -> hostEvent $ CEvtHostDisconnected p h - DOWN srv conns -> serverEvent srv NSDisconnected conns - UP srv conns -> serverEvent srv NSConnected conns + DOWN srv conns -> serverEvent srv SSPending conns + UP srv conns -> serverEvent srv SSActive conns SUSPENDED -> toView CEvtChatSuspended DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId ERRS cErrs -> errsEvent $ L.toList cErrs where hostEvent :: ChatEvent -> CM () hostEvent = whenM (asks $ hostEvents . config) . toView - serverEvent srv nsStatus conns = do - chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds - toView $ CEvtNetworkStatus srv nsStatus connIds - where - connIds = map AgentConnId conns + serverEvent :: SMPServer -> SubscriptionStatus -> [ConnId] -> CM () + serverEvent srv nsStatus conns = toView $ CEvtSubscriptionStatus srv nsStatus $ map AgentConnId conns errsEvent :: [(ConnId, AgentErrorType)] -> CM () errsEvent = toView . CEvtChatErrors . map (\(cId, e) -> ChatErrorAgent e (AgentConnId cId) Nothing) @@ -559,7 +556,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ct' = ct {activeConn = Just conn'} :: Contact -- [incognito] print incognito profile used for this contact incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - lift $ setContactNetworkStatus ct' NSConnected toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing -- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)? @@ -640,7 +636,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do - lift $ setContactNetworkStatus ct NSConnected toView $ CEvtContactSndReady user ct when (connChatVersion >= batchSend2Version) $ forM_ viaUserContactLink $ \userContactLinkId -> do (ucl, _) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId @@ -1455,7 +1450,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = notifyMemberConnected gInfo m ct_ = do (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m memberConnectedChatItem gInfo' scopeInfo m' - lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_ toView $ CEvtConnectedToGroupMember user gInfo' m' ct_ probeMatchingMembers :: Contact -> IncognitoEnabled -> CM () diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 4973e5b9bb..f5d07d18af 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -49,7 +49,6 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFile (..)) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index c9b5e020d9..24baa37e4e 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -46,7 +46,6 @@ import Data.Time.Clock (UTCTime, nominalDay) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types (User) -import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_) import Simplex.Messaging.Agent.Store.Entity diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 0b2b4ddc95..3c15a4303c 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -395,7 +395,7 @@ Query: JOIN connections c ON q.conn_id = c.conn_id WHERE c.deleted = 0 AND q.deleted = 0 Plan: -SCAN q USING INDEX idx_rcv_queues_link_id +SCAN q USING INDEX idx_rcv_queues_client_notice_id SEARCH c USING PRIMARY KEY (conn_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) USE TEMP B-TREE FOR DISTINCT @@ -477,6 +477,16 @@ Query: Plan: SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) +Query: + SELECT n.host, n.port, n.entity_id, COALESCE(n.server_key_hash, s.key_hash), n.created_at, n.notice_ttl + FROM client_notices n + JOIN servers s ON n.host = s.host AND n.port = s.port + WHERE n.protocol = 'smp' + +Plan: +SEARCH n USING INDEX idx_client_notices_entity (protocol=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) + Query: SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status FROM snd_messages s @@ -793,7 +803,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data @@ -808,7 +818,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data @@ -823,7 +833,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data @@ -838,7 +848,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data @@ -853,7 +863,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, - q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data @@ -867,7 +877,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) SEARCH c USING PRIMARY KEY (conn_id=?) Query: - SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, + SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port @@ -879,7 +889,7 @@ SEARCH q USING PRIMARY KEY (host=? AND port=?) SEARCH c USING PRIMARY KEY (conn_id=?) Query: - SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, + SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 2f4fd058e3..bb86cb2522 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -49,7 +49,6 @@ import Data.Word (Word16) import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) @@ -1789,26 +1788,6 @@ serializeIntroStatus = \case GMIntroToConnected -> "to-con" GMIntroConnected -> "con" -data NetworkStatus - = NSUnknown - | NSConnected - | NSDisconnected - | NSError {connectionError :: String} - deriving (Eq, Ord, Show) - -netStatusStr :: NetworkStatus -> String -netStatusStr = \case - NSUnknown -> "unknown" - NSConnected -> "subscribed" - NSDisconnected -> "disconnected" - NSError e -> "error: " <> e - -data ConnNetworkStatus = ConnNetworkStatus - { agentConnId :: AgentConnId, - networkStatus :: NetworkStatus - } - deriving (Show) - type CommandId = Int64 aCorrId :: CommandId -> ACorrId @@ -1998,10 +1977,6 @@ $(JQ.deriveJSON defaultJSON ''GroupMemberSettings) $(JQ.deriveJSON defaultJSON ''SecurityCode) -$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "NS") ''NetworkStatus) - -$(JQ.deriveJSON defaultJSON ''ConnNetworkStatus) - $(JQ.deriveJSON defaultJSON ''Connection) $(JQ.deriveJSON defaultJSON ''PendingContactConnection) diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs index d0f23a7307..096cda06f5 100644 --- a/src/Simplex/Chat/Types/UITheme.hs +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -14,7 +14,6 @@ import Data.Char (toLower) import Data.Maybe (fromMaybe) import Data.Text (Text) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) -import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) diff --git a/src/Simplex/Chat/Types/Util.hs b/src/Simplex/Chat/Types/Util.hs deleted file mode 100644 index afea178e41..0000000000 --- a/src/Simplex/Chat/Types/Util.hs +++ /dev/null @@ -1,8 +0,0 @@ -module Simplex.Chat.Types.Util where - -import qualified Data.Aeson as J -import qualified Data.Aeson.Types as JT -import Simplex.Messaging.Encoding.String - -textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a -textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3a41c5f767..bbccd514b4 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -232,7 +232,6 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRRcvStandaloneFileCreated u ft -> ttyUser u $ receivingFileStandalone "started" ft CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ - CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m CRMemberAccepted u g m -> ttyUser u $ viewMemberAccepted g m CRMemberSupportChatRead u g m -> ttyUser u $ viewSupportChatRead g m @@ -457,7 +456,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtSubscriptionEnd u acEntity -> let Connection {connId} = entityConnection acEntity in ttyUser u [sShow connId <> ": END"] - CEvtNetworkStatus srv status conns -> [plain $ netStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv] + CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv] CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r CEvtUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m @@ -1174,12 +1173,6 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString] viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"] viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"] -viewNetworkStatuses :: [ConnNetworkStatus] -> [StyledString] -viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortOn netStatus - where - netStatus ConnNetworkStatus {networkStatus} = networkStatus - viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s) - viewUserJoinedGroup :: GroupInfo -> [StyledString] viewUserJoinedGroup g@GroupInfo {membership} = case incognitoMembershipProfile g of @@ -1464,6 +1457,13 @@ viewConnDiffIds userDiff connDiff where showIds = plain . T.intercalate ", " . map (tshow . unwrapId) +subStatusStr :: SubscriptionStatus -> String +subStatusStr = \case + SSActive -> "subscribed" + SSPending -> "disconnected" + SSRemoved e -> "removed: " <> e + SSNoSub -> "no subscription" + viewUserServers :: UserOperatorServers -> [StyledString] viewUserServers (UserOperatorServers _ [] []) = [] viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index c6976fbe47..b4bce68535 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -180,8 +180,6 @@ chatDirectTests = do testReqVRange vr11 supportedChatVRange testReqVRange vr11 vr11 it "update peer version range on received messages" testUpdatePeerChatVRange - describe "network statuses" $ do - it "should get network statuses" testGetNetworkStatuses where testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 @@ -349,6 +347,7 @@ testRetryConnectingClientTimeout ps = do messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} } } + ChatConfig {presetServers = presetSrvs@PresetServers {netCfg}} = testCfg cfgZeroTimeout = (testCfg :: ChatConfig) { agentConfig = @@ -356,9 +355,7 @@ testRetryConnectingClientTimeout ps = do { quotaExceededTimeout = 1, messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} }, - presetServers = - let def@PresetServers {netCfg} = presetServers testCfg - in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}} + presetServers = presetSrvs {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}} } opts' = testOpts @@ -3324,18 +3321,6 @@ testUpdatePeerChatVRange ps = where cfg11 = testCfg {chatVRange = vr11} :: ChatConfig -testGetNetworkStatuses :: HasCallStack => TestParams -> IO () -testGetNetworkStatuses ps = do - withNewTestChat ps "alice" aliceProfile $ \alice -> do - withNewTestChat ps "bob" bobProfile $ \bob -> do - connectUsers alice bob - alice ##> "/_network_statuses" - alice <## "1 connections subscribed" - withTestChat ps "alice" $ \alice -> - withTestChat ps "bob" $ \bob -> do - alice <## "subscribed 1 connections on server localhost" - bob <## "subscribed 1 connections on server localhost" - vr11 :: VersionRangeChat vr11 = mkVersionRange (VersionChat 1) (VersionChat 1) diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index 0110c21fae..f02d04491e 100644 --- a/tests/JSONFixtures.hs +++ b/tests/JSONFixtures.hs @@ -28,12 +28,6 @@ chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" chatStartedTagged :: LB.ByteString chatStartedTagged = "{\"result\":{\"type\":\"chatStarted\"}}" -networkStatusesSwift :: LB.ByteString -networkStatusesSwift = "{\"result\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" - -networkStatusesTagged :: LB.ByteString -networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" - userJSON :: LB.ByteString userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}" diff --git a/tests/JSONTests.hs b/tests/JSONTests.hs index 33b601f6db..ed4d591547 100644 --- a/tests/JSONTests.hs +++ b/tests/JSONTests.hs @@ -25,7 +25,6 @@ owsf2TaggedJSONTest = do activeUserExistsSwift `to` activeUserExistsTagged activeUserSwift `to` activeUserTagged chatStartedSwift `to` chatStartedTagged - networkStatusesSwift `to` networkStatusesTagged parsedMarkdownSwift `to` parsedMarkdownTagged where to :: LB.ByteString -> LB.ByteString -> IO ()