diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a6d574e38d..57f4f2eda8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -561,6 +561,18 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro throw r } +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId)) if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 2e5a4f2af6..f5abfe9c58 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -110,6 +110,7 @@ struct ChatInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert + case queueInfo(info: String) case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { @@ -119,6 +120,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case let .queueInfo(info): return "queueInfo \(info)" case let .error(title, _): return "error \(title)" } } @@ -224,6 +226,18 @@ struct ChatInfoView: View { Section(header: Text("For console")) { infoRow("Local name", chat.chatInfo.localDisplayName) infoRow("Database ID", "\(chat.chatInfo.apiId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } } @@ -243,6 +257,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case let .queueInfo(info): return queueInfoAlert(info) case let .error(title, error): return mkAlert(title: title, message: error) } } @@ -577,6 +592,22 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al ) } +func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String { + let (rcvMsgInfo, qInfo) = info + var msgInfo: String + if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" } + return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo) +} + +func queueInfoAlert(_ info: String) -> Alert { + Alert( + title: Text("Message queue info"), + message: Text(info), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info } + ) +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index a24608b7e7..a851e3fc1d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -35,6 +35,7 @@ struct GroupMemberInfoView: View { case abortSwitchAddressAlert case syncConnectionForceAlert case planAndConnectAlert(alert: PlanAndConnectAlert) + case queueInfo(info: String) case error(title: LocalizedStringKey, error: LocalizedStringKey) var id: String { @@ -49,6 +50,7 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" + case let .queueInfo(info): return "queueInfo \(info)" case let .error(title, _): return "error \(title)" } } @@ -178,6 +180,18 @@ struct GroupMemberInfoView: View { Section("For console") { infoRow("Local name", member.localDisplayName) infoRow("Database ID", "\(member.groupMemberId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } } @@ -223,6 +237,7 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) + case let .queueInfo(info): return queueInfoAlert(info) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 93e61f3b21..412ecc1258 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -24,6 +24,11 @@ 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; + 5C0EA1272C08ADAF00AD2E5E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */; }; + 5C0EA1282C08ADAF00AD2E5E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1232C08ADAF00AD2E5E /* libffi.a */; }; + 5C0EA1292C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */; }; + 5C0EA12A2C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */; }; + 5C0EA12B2C08ADAF00AD2E5E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */; }; 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; }; 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; @@ -139,11 +144,6 @@ 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; - 5CEE879E2C076B8400583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87992C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a */; }; - 5CEE879F2C076B8400583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE879A2C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a */; }; - 5CEE87A02C076B8400583B8A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE879B2C076B8400583B8A /* libffi.a */; }; - 5CEE87A12C076B8400583B8A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE879C2C076B8400583B8A /* libgmp.a */; }; - 5CEE87A22C076B8400583B8A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE879D2C076B8400583B8A /* libgmpxx.a */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; @@ -273,6 +273,11 @@ 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; + 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C0EA1232C08ADAF00AD2E5E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a"; sourceTree = ""; }; + 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a"; sourceTree = ""; }; + 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; @@ -435,11 +440,6 @@ 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; - 5CEE87992C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a"; sourceTree = ""; }; - 5CEE879A2C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a"; sourceTree = ""; }; - 5CEE879B2C076B8400583B8A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CEE879C2C076B8400583B8A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CEE879D2C076B8400583B8A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; @@ -529,13 +529,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CEE879E2C076B8400583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CEE879F2C076B8400583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a in Frameworks */, - 5CEE87A02C076B8400583B8A /* libffi.a in Frameworks */, - 5CEE87A12C076B8400583B8A /* libgmp.a in Frameworks */, + 5C0EA1272C08ADAF00AD2E5E /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CEE87A22C076B8400583B8A /* libgmpxx.a in Frameworks */, + 5C0EA12A2C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a in Frameworks */, + 5C0EA1292C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a in Frameworks */, + 5C0EA1282C08ADAF00AD2E5E /* libffi.a in Frameworks */, + 5C0EA12B2C08ADAF00AD2E5E /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,11 +601,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CEE879B2C076B8400583B8A /* libffi.a */, - 5CEE879C2C076B8400583B8A /* libgmp.a */, - 5CEE879D2C076B8400583B8A /* libgmpxx.a */, - 5CEE879A2C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7-ghc9.6.3.a */, - 5CEE87992C076B8300583B8A /* libHSsimplex-chat-5.8.0.4-LLYprD9nQTR9mmjl4EdHW7.a */, + 5C0EA1232C08ADAF00AD2E5E /* libffi.a */, + 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */, + 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */, + 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */, + 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 12683bc3a4..3ce436aabb 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -82,6 +82,8 @@ public enum ChatCommand { case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiContactQueueInfo(contactId: Int64) + case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64) case apiSwitchContact(contactId: Int64) case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) case apiAbortSwitchContact(contactId: Int64) @@ -228,6 +230,8 @@ public enum ChatCommand { case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)" + case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)" case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" @@ -375,6 +379,8 @@ public enum ChatCommand { case .apiSetMemberSettings: return "apiSetMemberSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiContactQueueInfo: return "apiContactQueueInfo" + case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" case .apiSwitchContact: return "apiSwitchContact" case .apiSwitchGroupMember: return "apiSwitchGroupMember" case .apiAbortSwitchContact: return "apiAbortSwitchContact" @@ -516,6 +522,7 @@ public enum ChatResponse: Decodable, Error { case networkConfig(networkConfig: NetCfg) case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo) case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) @@ -678,6 +685,7 @@ public enum ChatResponse: Decodable, Error { case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" case .groupMemberInfo: return "groupMemberInfo" + case .queueInfo: return "queueInfo" case .contactSwitchStarted: return "contactSwitchStarted" case .groupMemberSwitchStarted: return "groupMemberSwitchStarted" case .contactSwitchAborted: return "contactSwitchAborted" @@ -836,6 +844,9 @@ public enum ChatResponse: Decodable, Error { case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") + case let .queueInfo(u, rcvMsgInfo, queueInfo): + let msgInfo = if let info = rcvMsgInfo { encodeJSON(rcvMsgInfo) } else { "none" } + return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))") case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") @@ -2170,3 +2181,42 @@ public enum UserNetworkType: String, Codable { } } } + +public struct RcvMsgInfo: Codable { + var msgId: Int64 + var msgDeliveryId: Int64 + var msgDeliveryStatus: String + var agentMsgId: Int64 + var agentMsgMeta: String +} + +public struct QueueInfo: Codable { + var qiSnd: Bool + var qiNtf: Bool + var qiSub: QSub? + var qiSize: Int + var qiMsg: MsgInfo? +} + +public struct QSub: Codable { + var qSubThread: QSubThread + var qDelivered: String? +} + +public enum QSubThread: String, Codable { + case noSub + case subPending + case subThread + case prohibitSub +} + +public struct MsgInfo: Codable { + var msgId: String + var msgTs: Date + var msgType: MsgType +} + +public enum MsgType: String, Codable { + case message + case quota +} diff --git a/cabal.project b/cabal.project index 575fcd100c..cbbe549649 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: 97a953550f90b14dbf0f9a62077e88df306ebeca + tag: 88f1b727e0e4bfef01a346d818b7f6d94a181b16 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 138aafe57a..f09bf7af57 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."97a953550f90b14dbf0f9a62077e88df306ebeca" = "15cdvaggk7jghyc0jsxgb17rl327a8z42h0pfga6yrcamcb07mln"; + "https://github.com/simplex-chat/simplexmq.git"."88f1b727e0e4bfef01a346d818b7f6d94a181b16" = "0i785dm8sm1c5lydq2kci1a3ix91q3hn89l7x80gnxyqgn4qq2gd"; "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/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 02a741ef41..54e9f31b4f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1379,6 +1379,11 @@ processChatCommand' vr = \case forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) + APIContactQueueInfo contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError $ CEContactNotActive ct APIGroupInfo gId -> withUser $ \user -> do (g, s) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) pure $ CRGroupInfo user g s @@ -1386,6 +1391,11 @@ processChatCommand' vr = \case (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo user g m connectionStats + APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError CEGroupMemberNotActive APISwitchContact contactId -> withUser $ \user -> do ct <- withStore $ \db -> getContact db vr user contactId case contactConnId ct of @@ -1497,6 +1507,8 @@ processChatCommand' vr = \case groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo + ContactQueueInfo cName -> withContactName cName APIContactQueueInfo + GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo SwitchContact cName -> withContactName cName APISwitchContact SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact @@ -2795,6 +2807,9 @@ processChatCommand' vr = \case pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci + getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do + msgInfo <- withStore' (`getLastRcvMsgInfo` connId) + CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) contactCITimed :: Contact -> CM (Maybe CITimed) contactCITimed ct = sndContactCITimed False ct Nothing @@ -7361,6 +7376,10 @@ chatCommandP = ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName), ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName), ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName), + "/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal), + "/_queue info @" *> (APIContactQueueInfo <$> A.decimal), + ("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayName <* A.space <* char_ '@' <*> displayName), + ("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayName), "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), "/_switch @" *> (APISwitchContact <$> A.decimal), "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8f8d460c60..9053f673f0 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -83,6 +83,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol) +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) @@ -357,6 +358,8 @@ data ChatCommand | APIContactInfo ContactId | APIGroupInfo GroupId | APIGroupMemberInfo GroupId GroupMemberId + | APIContactQueueInfo ContactId + | APIGroupMemberQueueInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId | APIAbortSwitchContact ContactId @@ -375,6 +378,8 @@ data ChatCommand | ContactInfo ContactName | ShowGroupInfo GroupName | GroupMemberInfo GroupName ContactName + | ContactQueueInfo ContactName + | GroupMemberQueueInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName | AbortSwitchContact ContactName @@ -569,6 +574,7 @@ data ChatResponse | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} | CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary} | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: QueueInfo} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 6c0e93e017..ea79161e06 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -962,6 +962,15 @@ data RcvMsgDelivery = RcvMsgDelivery } deriving (Show) +data RcvMsgInfo = RcvMsgInfo + { msgId :: Int64, + msgDeliveryId :: Int64, + msgDeliveryStatus :: Text, + agentMsgId :: AgentMsgId, + agentMsgMeta :: Text + } + deriving (Show) + data MsgMetaJSON = MsgMetaJSON { integrity :: Text, rcvId :: Int64, @@ -1332,3 +1341,5 @@ $(JQ.deriveJSON defaultJSON ''MsgMetaJSON) msgMetaJson :: MsgMeta -> Text msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson + +$(JQ.deriveJSON defaultJSON ''RcvMsgInfo) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 84b2536380..853d34995a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -23,6 +23,7 @@ module Simplex.Chat.Store.Messages createNewSndMessage, createSndMsgDelivery, createNewMessageAndRcvMsgDelivery, + getLastRcvMsgInfo, createNewRcvMessage, updateSndMsgDeliveryStatus, createPendingGroupMessage, @@ -226,6 +227,23 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent) pure msg +getLastRcvMsgInfo :: DB.Connection -> Int64 -> IO (Maybe RcvMsgInfo) +getLastRcvMsgInfo db connId = + maybeFirstRow rcvMsgInfo $ + DB.query + db + [sql| + SELECT message_id, msg_delivery_id, delivery_status, agent_msg_id, agent_msg_meta + FROM msg_deliveries + WHERE connection_id = ? AND delivery_status IN (?, ?) + ORDER BY created_at DESC, msg_delivery_id DESC + LIMIT 1 + |] + (connId, MDSRcvAgent, MDSRcvAcknowledged) + where + rcvMsgInfo (msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta) = + RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta} + createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember = case connOrGroupId of diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index b912040ac5..a315e755e1 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -13,7 +13,6 @@ module Simplex.Chat.View where import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) @@ -65,7 +64,7 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Util (tshow) import Simplex.Messaging.Version hiding (version) import Simplex.RemoteControl.Types (RCCtrlAddress (..)) import System.Console.ANSI.Types @@ -90,10 +89,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] CRChatSuspended -> ["chat suspended"] - CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats - CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] - CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] + CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRApiParsedMarkdown ft -> [viewJSON ft] CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl @@ -101,6 +100,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats + CRQueueInfo _ msgInfo qInfo -> + [ "last received msg: " <> maybe "none" viewJSON msgInfo, + "server queue info: " <> viewJSON qInfo + ] CRContactSwitchStarted {} -> ["switch started"] CRGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] @@ -220,7 +223,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] - CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [plain . LB.toStrict $ J.encode j]) info_ + CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRContactConnecting u _ -> ttyUser u [] CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] @@ -322,7 +325,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] - <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ + <> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> [ ("remote controller " <> sShow remoteCtrlId <> " found: ") @@ -354,8 +357,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, - plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), - plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) + "chat entity locks: " <> viewJSON chatEntityLocks, + "agent locks: " <> viewJSON agentLocks ] CRAgentStats stats -> map (plain . intercalate ",") stats CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> @@ -370,12 +373,12 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ("active subscriptions:" : map sShow activeSubscriptions) <> ("pending subscriptions: " : map sShow pendingSubscriptions) <> ("removed subscriptions: " : map sShow removedSubscriptions) - CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)] + CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> viewJSON agentWorkersSummary] CRAgentWorkersDetails {agentWorkersDetails} -> [ "agent workers details:", - plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line + viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line ] - CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", plain . LB.unpack $ J.encode msgCounts] + CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", viewJSON msgCounts] CRAgentQueuesInfo {agentQueuesInfo} -> [ "agent queues info:", plain . LB.unpack $ J.encode agentQueuesInfo @@ -397,7 +400,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] - CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)] + CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] CRCustomChatResponse u r -> ttyUser' u $ [plain r] where @@ -1241,10 +1244,10 @@ viewGroupInfo GroupInfo {groupId, uiThemes, customData} s = <> viewCustomData customData viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString] -viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)]) +viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes]) viewCustomData :: Maybe CustomData -> [StyledString] -viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)]) +viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats = @@ -1686,7 +1689,7 @@ receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatIt receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) = [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr where - cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"] + cfArgsStr (Just cfArgs) = [cryptoFileArgsStr testView cfArgs | status == "completed"] cfArgsStr _ = [] getRemoteFileStr = case hu of (Just rhId, Just User {userId}) @@ -1707,10 +1710,10 @@ viewLocalFile to CIFile {fileId, fileSource} ts tz = case fileSource of Just (CryptoFile fPath _) -> sentWithTime_ ts tz [to <> fileTransferStr fileId fPath] _ -> const [] -cryptoFileArgsStr :: Bool -> CryptoFileArgs -> ByteString +cryptoFileArgsStr :: Bool -> CryptoFileArgs -> StyledString cryptoFileArgsStr testView cfArgs@(CFArgs key nonce) - | testView = LB.toStrict $ J.encode cfArgs - | otherwise = "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce + | testView = viewJSON cfArgs + | otherwise = plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce fileFrom :: ChatInfo c -> CIDirection c d -> StyledString fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct @@ -1828,7 +1831,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand [ ttyContact' ct <> " continued the WebRTC call", "To connect, please paste the data below in your browser window you opened earlier and click Connect button", "", - plain . LB.toStrict . J.encode $ WCCallAnswer {answer, iceCandidates} + viewJSON WCCallAnswer {answer, iceCandidates} ] callMediaStr :: CallType -> StyledString @@ -2086,6 +2089,9 @@ viewConnectionEntityInactive entity inactive | inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"] | otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"] +viewJSON :: J.ToJSON a => a -> StyledString +viewJSON = plain . LB.toStrict . J.encode + connEntityLabel :: ConnectionEntity -> StyledString connEntityLabel = \case RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c