diff --git a/README.md b/README.md index 697f5ca08e..4e74bbc866 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + [Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) [Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) @@ -382,10 +384,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ Private notes. - ✅ Improve sending videos (including encryption of locally stored videos). - ✅ Post-quantum resistant key exchange in double ratchet protocol. +- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - 🏗 Improve stability and reduce battery usage. - 🏗 Improve experience for the new users. - 🏗 Large groups, communities and public channels. -- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - Privacy & security slider - a simple way to set all settings at once. - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 764db8335e..49152283ee 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1826,11 +1826,15 @@ func processReceivedMsg(_ res: ChatResponse) async { if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) } - case let .rcvFileError(user, aChatItem, _): + case let .rcvFileError(user, aChatItem, _, _): if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } } + case let .rcvFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .sndFileStart(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) case let .sndFileComplete(user, aChatItem, _): @@ -1852,6 +1856,10 @@ func processReceivedMsg(_ res: ChatResponse) async { await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } } + case let .sndFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .callInvitation(invitation): await MainActor.run { m.callInvitations[invitation.contact.id] = invitation @@ -1949,12 +1957,28 @@ func processReceivedMsg(_ res: ChatResponse) async { let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "") m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state) } - case .remoteCtrlStopped: + case let .remoteCtrlStopped(_, rcStopReason): // This delay is needed to cancel the session that fails on network failure, // e.g. when user did not grant permission to access local network yet. if let sess = m.remoteCtrlSession { await MainActor.run { m.remoteCtrlSession = nil + dismissAllSheets() { + switch rcStopReason { + case .connectionFailed(.errorAgent(.RCP(.identity))): + AlertManager.shared.showAlertMsg( + title: "Connection with desktop stopped", + message: "This link was used with another mobile device, please create a new link on the desktop." + ) + default: + AlertManager.shared.showAlert(Alert( + title: Text("Connection with desktop stopped"), + message: Text("Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers."), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) } + )) + } + } } if case .connected = sess.sessionState { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 53d840306e..6def90ebe9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -56,14 +56,16 @@ struct CIFileView: View { case .sndTransfer: return false case .sndComplete: return true case .sndCancelled: return false - case .sndError: return false + case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return true case .rcvAccepted: return true case .rcvTransfer: return false case .rcvAborted: return true case .rcvComplete: return true case .rcvCancelled: return false - case .rcvError: return false + case .rcvError: return true + case .rcvWarning: return true case .invalid: return false } } @@ -108,6 +110,18 @@ struct CIFileView: View { if let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case let .rcvError(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvError") + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + case let .rcvWarning(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvWarning") + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) case .sndStored: logger.debug("CIFileView fileAction - in .sndStored") if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) { @@ -118,6 +132,18 @@ struct CIFileView: View { if let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case let .sndError(sndFileError): + logger.debug("CIFileView fileAction - in .sndError") + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + case let .sndWarning(sndFileError): + logger.debug("CIFileView fileAction - in .sndWarning") + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) default: break } } @@ -141,6 +167,7 @@ struct CIFileView: View { case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10) case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .rcvInvitation: if fileSizeValid(file) { fileIcon("arrow.down.doc.fill", color: .accentColor) @@ -159,6 +186,7 @@ struct CIFileView: View { case .rcvComplete: fileIcon("doc.fill") case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10) } } else { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 7d33df6c60..cfead635fe 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -60,6 +60,26 @@ struct CIImageView: View { case .rcvTransfer: () // ? case .rcvComplete: () // ? case .rcvCancelled: () // TODO + case let .rcvError(rcvFileError): + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + case let .rcvWarning(rcvFileError): + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + case let .sndError(sndFileError): + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + case let .sndWarning(sndFileError): + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) default: () } } @@ -99,14 +119,16 @@ struct CIImageView: View { case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) case .sndError: fileIcon("xmark", 10, 13) + case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvTransfer: progressView() case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13) + case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index 3bfe24b79d..d84ad8f5fc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -192,7 +192,7 @@ struct CIVideoView: View { .disabled(!canBePlayed) } } - loadingIndicator() + fileStatusIcon() } .onAppear { addObserver(player, url) @@ -258,11 +258,11 @@ struct CIVideoView: View { .resizable() .scaledToFit() .frame(width: w) - loadingIndicator() + fileStatusIcon() } } - @ViewBuilder private func loadingIndicator() -> some View { + @ViewBuilder private func fileStatusIcon() -> some View { if let file = chatItem.file { switch file.fileStatus { case .sndStored: @@ -279,7 +279,22 @@ struct CIVideoView: View { } case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) - case .sndError: fileIcon("xmark", 10, 13) + case let .sndError(sndFileError): + fileIcon("xmark", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + } + case let .sndWarning(sndFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) + } case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case let .rcvTransfer(rcvProgress, rcvTotal): @@ -289,10 +304,25 @@ struct CIVideoView: View { progressView() } case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) - case .rcvError: fileIcon("xmark", 10, 13) + case let .rcvError(rcvFileError): + fileIcon("xmark", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + } + case let .rcvWarning(rcvFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + } case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index ba1712f6e3..4d950a0d99 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -134,18 +134,53 @@ struct VoiceMessagePlayer: View { ZStack { if let recordingFile = recordingFile { switch recordingFile.fileStatus { - case .sndStored: playbackButton() - case .sndTransfer: playbackButton() + case .sndStored: + if recordingFile.fileProtocol == .local { + playbackButton() + } else { + loadingIcon() + } + case .sndTransfer: loadingIcon() case .sndComplete: playbackButton() case .sndCancelled: playbackButton() - case .sndError: playbackButton() + case let .sndError(sndFileError): + fileStatusIcon("multiply", 14) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + } + case let .sndWarning(sndFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) + } case .rcvInvitation: downloadButton(recordingFile, "play.fill") case .rcvAccepted: loadingIcon() case .rcvTransfer: loadingIcon() case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath") case .rcvComplete: playbackButton() case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) - case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) + case let .rcvError(rcvFileError): + fileStatusIcon("multiply", 14) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + } + case let .rcvWarning(rcvFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + } case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) } } else { @@ -246,6 +281,17 @@ struct VoiceMessagePlayer: View { } } + private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size, height: size) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: 56, height: 56) + .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .clipShape(Circle()) + } + private func loadingIcon() -> some View { ProgressView() .frame(width: 30, height: 30) diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 29bfdb6288..f5da473fd6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -17,6 +17,8 @@ struct ChatItemInfoView: View { @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history @State private var alert: CIInfoViewAlert? = nil + @State private var messageStatusLimited: Bool = true + @State private var fileStatusLimited: Bool = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum CIInfoTab { @@ -157,6 +159,35 @@ struct ChatItemInfoView: View { if developerTools { infoRow("Database ID", "\(meta.itemId)") infoRow("Record updated at", localTimestamp(meta.updatedAt)) + let msv = infoRow("Message status", ci.meta.itemStatus.id) + Group { + if messageStatusLimited { + msv.lineLimit(1) + } else { + msv + } + } + .onTapGesture { + withAnimation { + messageStatusLimited.toggle() + } + } + + if let file = ci.file { + let fsv = infoRow("File status", file.fileStatus.id) + Group { + if fileStatusLimited { + fsv.lineLimit(1) + } else { + fsv + } + } + .onTapGesture { + withAnimation { + fileStatusLimited.toggle() + } + } + } } } } @@ -473,8 +504,12 @@ struct ChatItemInfoView: View { if developerTools { shareText += [ String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId), - String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)) + String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)), + String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id) ] + if let file = ci.file { + shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)] + } } if let qi = ci.quotedItem { shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 7ece4fdee6..442f933ace 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -367,11 +367,12 @@ struct ScannerInView: View { @Binding var showQRCodeScanner: Bool let processQRCode: (_ resp: Result) -> Void @State private var cameraAuthorizationStatus: AVAuthorizationStatus? + var scanMode: ScanMode = .continuous var body: some View { Group { if showQRCodeScanner, case .authorized = cameraAuthorizationStatus { - CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .listRowBackground(Color.clear) @@ -436,6 +437,7 @@ struct ScannerInView: View { } } + private func linkTextView(_ link: String) -> some View { Text(link) .lineLimit(1) diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 3059b049a3..c749e09ca8 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -181,23 +181,27 @@ struct ConnectDesktopView: View { } private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View { - List { - Section("Connecting to desktop") { - ctrlDeviceNameText(session, rc) - ctrlDeviceVersionText(session) - } + ZStack { + List { + Section("Connecting to desktop") { + ctrlDeviceNameText(session, rc) + ctrlDeviceVersionText(session) + } - if let sessCode = session.sessionCode { - Section("Session code") { - sessionCodeText(sessCode) + if let sessCode = session.sessionCode { + Section("Session code") { + sessionCodeText(sessCode) + } + } + + Section { + disconnectButton() } } + .navigationTitle("Connecting to desktop") - Section { - disconnectButton() - } + ProgressView().scaleEffect(2) } - .navigationTitle("Connecting to desktop") } private func searchingDesktopView() -> some View { @@ -329,16 +333,10 @@ struct ConnectDesktopView: View { } } } - + private func scanDesctopAddressView() -> some View { Section("Scan QR code from desktop") { - CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode) - .aspectRatio(1, contentMode: .fit) - .cornerRadius(12) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .padding(.horizontal) + ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ed889b5218..a1e2eeb66b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1552,7 +1552,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1601,7 +1601,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1687,7 +1687,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1724,7 +1724,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1761,7 +1761,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1812,7 +1812,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 223; + CURRENT_PROJECT_VERSION = 224; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1574d43ac0..7b0a0a6646 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -622,7 +622,8 @@ public enum ChatResponse: Decodable, Error { case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer) case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) + case rcvFileError(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + case rcvFileWarning(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) // sending file events case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) @@ -636,6 +637,7 @@ public enum ChatResponse: Decodable, Error { case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String]) case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) // call events case callInvitation(callInvitation: RcvCallInvitation) case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) @@ -784,6 +786,7 @@ public enum ChatResponse: Decodable, Error { case .rcvFileCancelled: return "rcvFileCancelled" case .rcvFileSndCancelled: return "rcvFileSndCancelled" case .rcvFileError: return "rcvFileError" + case .rcvFileWarning: return "rcvFileWarning" case .sndFileStart: return "sndFileStart" case .sndFileComplete: return "sndFileComplete" case .sndFileCancelled: return "sndFileCancelled" @@ -796,6 +799,7 @@ public enum ChatResponse: Decodable, Error { case .sndStandaloneFileComplete: return "sndStandaloneFileComplete" case .sndFileCancelledXFTP: return "sndFileCancelledXFTP" case .sndFileError: return "sndFileError" + case .sndFileWarning: return "sndFileWarning" case .callInvitation: return "callInvitation" case .callOffer: return "callOffer" case .callAnswer: return "callAnswer" @@ -944,7 +948,8 @@ public enum ChatResponse: Decodable, Error { case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .rcvFileError(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .rcvFileWarning(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) @@ -957,6 +962,7 @@ public enum ChatResponse: Decodable, Error { case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count)) case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") case let .callInvitation(inv): return String(describing: inv) case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") @@ -974,7 +980,7 @@ public enum ChatResponse: Decodable, Error { case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) - case .remoteCtrlStopped: return noDetails + case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))" case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails @@ -1771,8 +1777,6 @@ public enum ChatErrorType: Decodable { case fileImageSize(filePath: String) case fileNotReceived(fileId: Int64) case fileNotApproved(fileId: Int64, unknownServers: [String]) - // case xFTPRcvFile - // case xFTPSndFile case fallbackToSMPProhibited(fileId: Int64) case inlineFileProhibited(fileId: Int64) case invalidQuote diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 27e5a9818c..4c62fe4c8b 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2728,7 +2728,7 @@ public enum CIStatus: Decodable { case rcvRead case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndNew: return "sndNew" case .sndSent: return "sndSent" @@ -2809,11 +2809,19 @@ public enum SndError: Decodable { } } -public enum SrvError: Decodable { +public enum SrvError: Decodable, Equatable { case host case version case other(srvError: String) + var id: String { + switch self { + case .host: return "host" + case .version: return "version" + case let .other(srvError): return "other \(srvError)" + } + } + public var errorInfo: String { switch self { case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.") @@ -3171,6 +3179,7 @@ public struct CIFile: Decodable { case .sndComplete: return true case .sndCancelled: return true case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return false case .rcvAccepted: return false case .rcvTransfer: return false @@ -3178,6 +3187,7 @@ public struct CIFile: Decodable { case .rcvCancelled: return false case .rcvComplete: return true case .rcvError: return false + case .rcvWarning: return false case .invalid: return false } } @@ -3196,12 +3206,14 @@ public struct CIFile: Decodable { } case .sndCancelled: return nil case .sndError: return nil + case .sndWarning: return sndCancelAction case .rcvInvitation: return nil case .rcvAccepted: return rcvCancelAction case .rcvTransfer: return rcvCancelAction case .rcvAborted: return nil case .rcvCancelled: return nil case .rcvComplete: return nil + case .rcvWarning: return rcvCancelAction case .rcvError: return nil case .invalid: return nil } @@ -3310,35 +3322,64 @@ public enum CIFileStatus: Decodable, Equatable { case sndTransfer(sndProgress: Int64, sndTotal: Int64) case sndComplete case sndCancelled - case sndError + case sndError(sndFileError: FileError) + case sndWarning(sndFileError: FileError) case rcvInvitation case rcvAccepted case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) case rcvAborted case rcvComplete case rcvCancelled - case rcvError + case rcvError(rcvFileError: FileError) + case rcvWarning(rcvFileError: FileError) case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndStored: return "sndStored" case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)" case .sndComplete: return "sndComplete" case .sndCancelled: return "sndCancelled" - case .sndError: return "sndError" + case let .sndError(sndFileError): return "sndError \(sndFileError)" + case let .sndWarning(sndFileError): return "sndWarning \(sndFileError)" case .rcvInvitation: return "rcvInvitation" case .rcvAccepted: return "rcvAccepted" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" case .rcvAborted: return "rcvAborted" case .rcvComplete: return "rcvComplete" case .rcvCancelled: return "rcvCancelled" - case .rcvError: return "rcvError" + case let .rcvError(rcvFileError): return "rcvError \(rcvFileError)" + case let .rcvWarning(rcvFileError): return "rcvWarning \(rcvFileError)" case .invalid: return "invalid" } } } +public enum FileError: Decodable, Equatable { + case auth + case noFile + case relay(srvError: SrvError) + case other(fileError: String) + + var id: String { + switch self { + case .auth: return "auth" + case .noFile: return "noFile" + case let .relay(srvError): return "relay \(srvError)" + case let .other(fileError): return "other \(fileError)" + } + } + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text") + case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo) + case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError) + } + } +} + public enum MsgContent: Equatable { case text(String) case link(text: String, preview: LinkPreview) 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 74a54e54cf..89af46edb5 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 @@ -2660,6 +2660,7 @@ data class CIFile( is CIFileStatus.SndComplete -> true is CIFileStatus.SndCancelled -> true is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false @@ -2667,6 +2668,7 @@ data class CIFile( is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false + is CIFileStatus.RcvWarning -> false is CIFileStatus.Invalid -> false } @@ -2682,6 +2684,7 @@ data class CIFile( } is CIFileStatus.SndCancelled -> null is CIFileStatus.SndError -> null + is CIFileStatus.SndWarning -> sndCancelAction is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction @@ -2689,6 +2692,7 @@ data class CIFile( is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null + is CIFileStatus.RcvWarning -> rcvCancelAction is CIFileStatus.Invalid -> null } @@ -2862,14 +2866,16 @@ sealed class CIFileStatus { @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus() @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus() @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus() - @Serializable @SerialName("sndError") object SndError: CIFileStatus() + @Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() - @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() + @Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus() + @Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus() val sent: Boolean get() = when (this) { @@ -2878,6 +2884,7 @@ sealed class CIFileStatus { is SndComplete -> true is SndCancelled -> true is SndError -> true + is SndWarning -> true is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false @@ -2885,10 +2892,26 @@ sealed class CIFileStatus { is RcvComplete -> false is RcvCancelled -> false is RcvError -> false + is RcvWarning -> false is Invalid -> false } } +@Serializable +sealed class FileError { + @Serializable @SerialName("auth") class Auth: FileError() + @Serializable @SerialName("noFile") class NoFile: FileError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError() + @Serializable @SerialName("other") class Other(val fileError: String): FileError() + + val errorInfo: String get() = when (this) { + is FileError.Auth -> generalGetString(MR.strings.file_error_auth) + is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file) + is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo) + is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError) + } +} + @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = MsgContentSerializer::class) sealed class MsgContent { 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 1f7f55fb53..c55ea0a871 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 @@ -1,9 +1,18 @@ package chat.simplex.common.model +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatModel.updatingChatsMutex @@ -12,7 +21,6 @@ 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.group.toggleShowMemberMessages import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* @@ -20,6 +28,7 @@ import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import chat.simplex.res.MR import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.withLock @@ -2026,6 +2035,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.RcvFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.SndFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.SndFileComplete -> { @@ -2052,6 +2066,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.SndFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.CallInvitation -> { chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId)) } @@ -2184,15 +2203,43 @@ object ChatController { val sess = chatModel.remoteCtrlSession.value if (sess != null) { chatModel.remoteCtrlSession.value = null + ModalManager.fullscreen.closeModals() fun showAlert(chatError: ChatError) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.remote_ctrl_was_disconnected_title), - if (chatError is ChatError.ChatErrorRemoteCtrl) { - chatError.remoteCtrlError.localizedString - } else { - generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string) - } - ) + when { + r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed + && r.rcStopReason.chatError is ChatError.ChatErrorAgent + && r.rcStopReason.chatError.agentError is AgentErrorType.RCP + && r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = generalGetString(MR.strings.remote_ctrl_connection_stopped_identity_desc) + ) + else -> + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = if (chatError is ChatError.ChatErrorRemoteCtrl) { + chatError.remoteCtrlError.localizedString + } else { + generalGetString(MR.strings.remote_ctrl_connection_stopped_desc) + }, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + val clipboard = LocalClipboardManager.current + SectionItemView({ + clipboard.setText(AnnotatedString(json.encodeToString(r.rcStopReason))) + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.copy_error), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } } when (r.rcStopReason) { is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError) @@ -4322,6 +4369,7 @@ sealed class CR { @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileWarning") class RcvFileWarning(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() // sending file events @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @@ -4334,7 +4382,8 @@ sealed class CR { @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List): CR() @Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() - @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() + @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() + @Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() // call events @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List): CR() @@ -4493,6 +4542,7 @@ sealed class CR { is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" is RcvFileError -> "rcvFileError" + is RcvFileWarning -> "rcvFileWarning" is SndFileStart -> "sndFileStart" is SndFileComplete -> "sndFileComplete" is SndFileRcvCancelled -> "sndFileRcvCancelled" @@ -4502,6 +4552,7 @@ sealed class CR { is SndStandaloneFileComplete -> "sndStandaloneFileComplete" is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" + is SndFileWarning -> "sndFileWarning" is CallInvitations -> "callInvitations" is CallInvitation -> "callInvitation" is CallOffer -> "callOffer" @@ -4652,6 +4703,7 @@ sealed class CR { is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") is RcvStandaloneFileComplete -> withUser(user, targetPath) is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") + is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") is SndFileCancelled -> json.encodeToString(chatItem_) is SndStandaloneFileCreated -> noDetails() is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) @@ -4663,7 +4715,8 @@ sealed class CR { is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString()) is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_)) - is SndFileError -> withUser(user, json.encodeToString(chatItem_)) + is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") + is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}" is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") @@ -4700,7 +4753,7 @@ sealed class CR { (if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) + "\nsessionCode: $sessionCode" is RemoteCtrlConnected -> json.encodeToString(remoteCtrl) - is RemoteCtrlStopped -> noDetails() + is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason" is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption") is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled") is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + @@ -4982,8 +5035,6 @@ sealed class ChatErrorType { is FileImageSize -> "fileImageSize" is FileNotReceived -> "fileNotReceived" is FileNotApproved -> "fileNotApproved" - // is XFTPRcvFile -> "xftpRcvFile" - // is XFTPSndFile -> "xftpSndFile" is FallbackToSMPProhibited -> "fallbackToSMPProhibited" is InlineFileProhibited -> "inlineFileProhibited" is InvalidQuote -> "invalidQuote" @@ -5061,8 +5112,6 @@ sealed class ChatErrorType { @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() @Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List): ChatErrorType() - // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() - // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 85179d66c7..d6b8901042 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -1,6 +1,7 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.views.helpers.* @@ -57,6 +58,9 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat try { if (chatModel.ctrlInitInProgress.value) return chatModel.ctrlInitInProgress.value = true + if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) { + ksDatabasePassword.remove() + } val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp var migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 2f8f6ed0bf..e047f82837 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -224,6 +224,8 @@ object ThemeManager { s.length == 1 -> "#ff$s$s$s$s$s$s" s.length == 2 -> "#ff$s$s$s" s.length == 3 -> "#ff$s$s" + s.length == 4 && this.alpha == 0f -> "#0000$s" // 000088ff treated as 88ff + s.length == 4 -> "#ff00$s" s.length == 6 && this.alpha == 0f -> "#00$s" s.length == 6 -> "#ff$s" else -> "#$s" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 1e564db134..c23a6ad00e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -31,6 +31,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import kotlinx.serialization.encodeToString sealed class CIInfoTab { class Delivery(val memberDeliveryStatuses: List): CIInfoTab() @@ -216,6 +217,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } + @Composable + fun ExpandableInfoRow(title: String, value: String) { + val expanded = remember { mutableStateOf(false) } + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = 46.dp) + .padding(PaddingValues(horizontal = DEFAULT_PADDING)) + .clickable { expanded.value = !expanded.value }, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, color = MaterialTheme.colors.onBackground) + if (expanded.value) { + Text(value, color = MaterialTheme.colors.secondary) + } else { + Text(value, color = MaterialTheme.colors.secondary, maxLines = 1) + } + } + } + @Composable fun Details() { AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) @@ -244,6 +266,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (devTools) { InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString()) InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) + ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus)) + if (ci.file != null) { + ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus)) + } } } } @@ -531,6 +557,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem if (devTools) { shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) + shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus))) + if (ci.file != null) { + shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus))) + } } val qi = ci.quotedItem if (qi != null) { 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 71d82b5691..af9d77b1d0 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 @@ -962,17 +962,8 @@ fun BoxWithConstraintsScope.ChatItemsList( // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } - val directions = setOf(DismissDirection.EndToStart) - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = directions, - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) - val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) - if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { - LaunchedEffect(Unit) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { scope.launch { if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { @@ -983,7 +974,13 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } + false } + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) val provider = { providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> scope.launch { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 0ce9ba32fc..89751dd140 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -87,6 +87,26 @@ fun CIFileView( ) FileProtocol.LOCAL -> {} } + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) file.forwardingAllowed() -> { withLongRunningApi(slow = 600_000) { var filePath = getLoadedFilePath(file) @@ -157,6 +177,7 @@ fun CIFileView( is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.RcvInvitation -> if (fileSizeValid(file)) fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) @@ -174,6 +195,7 @@ fun CIFileView( is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark)) } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 7cffe4564b..5aa3bfab05 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -70,14 +70,16 @@ fun CIImageView( is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvComplete -> {} is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } @@ -201,8 +203,8 @@ fun CIImageView( } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { - when (file.fileStatus) { - CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> + when { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> if (fileSizeValid()) { receiveFile(file.fileId) } else { @@ -211,7 +213,7 @@ fun CIImageView( String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) ) } - CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -225,9 +227,29 @@ fun CIImageView( ) FileProtocol.LOCAL -> {} } - CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? - CIFileStatus.RcvComplete -> {} // ? - CIFileStatus.RcvCancelled -> {} // TODO + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvTransfer -> {} // ? + file.fileStatus is CIFileStatus.RcvComplete -> {} // ? + file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO else -> {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index 749816f918..e655b73b02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -107,7 +107,7 @@ fun CIVideoView( } } } - loadingIndicator(file) + fileStatusIcon(file) } } @@ -339,11 +339,13 @@ private fun progressIndicator() { } @Composable -private fun fileIcon(icon: Painter, stringId: StringResource) { +private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) { + var modifier = Modifier.fillMaxSize() + modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier } Icon( icon, stringResource(stringId), - Modifier.fillMaxSize(), + modifier, tint = Color.White ) } @@ -364,7 +366,7 @@ private fun progressCircle(progress: Long, total: Long) { } @Composable -private fun loadingIndicator(file: CIFile?) { +private fun fileStatusIcon(file: CIFile?) { if (file != null) { Box( Modifier @@ -387,7 +389,28 @@ private fun loadingIndicator(file: CIFile?) { } is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + is CIFileStatus.SndWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) is CIFileStatus.RcvTransfer -> @@ -397,10 +420,31 @@ private fun loadingIndicator(file: CIFile?) { progressIndicator() } is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) + is CIFileStatus.RcvComplete -> {} is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + is CIFileStatus.RcvWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 96aaf586e9..040dd97474 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -252,6 +252,81 @@ private fun PlayPauseButton( } } +@Composable +private fun PlayablePlayPauseButton( + audioPlaying: Boolean, + sent: Boolean, + hasText: Boolean, + progress: State, + duration: State, + strokeWidth: Float, + strokeColor: Color, + error: Boolean, + play: () -> Unit, + pause: () -> Unit, + longClick: () -> Unit, +) { + val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() + if (hasText) { + IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { + Icon( + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), + contentDescription = null, + Modifier.size(36.dp), + tint = MaterialTheme.colors.primary + ) + } + } else { + PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) + } +} + +@Composable +private fun VoiceMsgLoadingProgressIndicator() { + Box( + Modifier + .size(56.dp) + .clip(RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center + ) { + ProgressIndicator() + } +} + +@Composable +private fun FileStatusIcon( + sent: Boolean, + icon: ImageResource, + longClick: () -> Unit, + onClick: () -> Unit, +) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Surface( + color = if (sent) sentColor else receivedColor, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .combinedClickable( + onClick = onClick, + onLongClick = longClick + ) + .onRightClick { longClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(icon), + contentDescription = null, + Modifier.size(36.dp), + tint = MaterialTheme.colors.secondary + ) + } + } +} + @Composable private fun VoiceMsgIndicator( file: CIFile?, @@ -268,39 +343,73 @@ private fun VoiceMsgIndicator( ) { val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } val strokeColor = MaterialTheme.colors.primary - if (file != null && file.loaded && progress != null && duration != null) { - val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() - if (hasText) { - IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { - Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), - contentDescription = null, - Modifier.size(36.dp), - tint = MaterialTheme.colors.primary - ) + when { + file?.fileStatus is CIFileStatus.SndStored -> + if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) { + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + } else { + VoiceMsgLoadingProgressIndicator() } - } else { - PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) - } - } else { - if (file?.fileStatus is CIFileStatus.RcvInvitation) { + file?.fileStatus is CIFileStatus.SndTransfer -> + VoiceMsgLoadingProgressIndicator() + file != null && file.fileStatus is CIFileStatus.SndError -> + FileStatusIcon( + sent, + MR.images.ic_close, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.SndWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file?.fileStatus is CIFileStatus.RcvInvitation -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) - } else if (file?.fileStatus is CIFileStatus.RcvTransfer - || file?.fileStatus is CIFileStatus.RcvAccepted - ) { - Box( - Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } else if (file?.fileStatus is CIFileStatus.RcvAborted) { + file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted -> + VoiceMsgLoadingProgressIndicator() + file?.fileStatus is CIFileStatus.RcvAborted -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) - } else { + file != null && file.fileStatus is CIFileStatus.RcvError -> + FileStatusIcon( + sent, + MR.images.ic_close, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.RcvWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.loaded && progress != null && duration != null -> + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick) + else -> PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) - } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 8969ca9b21..b2777a7042 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -189,13 +189,6 @@ fun FramedItemView( val receivedColor = MaterialTheme.appColors.receivedMessage Box(Modifier .clip(RoundedCornerShape(18.dp)) - .background( - when { - transparentBackground -> Color.Transparent - sent -> MaterialTheme.colors.background - else -> MaterialTheme.colors.background - } - ) .background( when { transparentBackground -> Color.Transparent diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index ca14b19adf..a2aeac21a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -40,9 +41,8 @@ import kotlin.math.log2 @Composable fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { val progressIndicator = remember { mutableStateOf(false) } - val prefs = m.controller.appPrefs - val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } - val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } + val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) } + val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) } val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } @@ -54,7 +54,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { ) { DatabaseEncryptionLayout( useKeychain, - prefs, m.chatDbEncrypted.value, currentKey, newKey, @@ -65,7 +64,16 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { migration, onConfirmEncrypt = { withLongRunningApi { - encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration) + encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = initialRandomDBPassphrase, + useKeychain = useKeychain, + storedKey = storedKey, + progressIndicator = progressIndicator, + migration = migration + ) } } ) @@ -89,7 +97,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { @Composable fun DatabaseEncryptionLayout( useKeychain: MutableState, - prefs: AppPreferences, chatDbEncrypted: Boolean?, currentKey: MutableState, newKey: MutableState, @@ -119,14 +126,14 @@ fun DatabaseEncryptionLayout( enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration ) { checked -> if (checked) { - setUseKeychain(true, useKeychain, prefs, migration) + setUseKeychain(true, useKeychain, migration) } else if (storedKey.value && !migration) { // Don't show in migration process since it will remove the key after successful encryption removePassphraseAlert { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false) + removePassphraseFromKeyChain(useKeychain, storedKey, false) } } else { - setUseKeychain(false, useKeychain, prefs, migration) + setUseKeychain(false, useKeychain, migration) } } @@ -272,17 +279,17 @@ fun resetFormAfterEncryption( m.controller.appPrefs.initialRandomDBPassphrase.set(false) } -fun setUseKeychain(value: Boolean, useKeychain: MutableState, prefs: AppPreferences, migration: Boolean) { +fun setUseKeychain(value: Boolean, useKeychain: MutableState, migration: Boolean) { useKeychain.value = value // Postpone it when migrating to the end of encryption process if (!migration) { - prefs.storeDBPassphrase.set(value) + appPrefs.storeDBPassphrase.set(value) } } -private fun removePassphraseFromKeyChain(useKeychain: MutableState, prefs: AppPreferences, storedKey: MutableState, migration: Boolean) { +private fun removePassphraseFromKeyChain(useKeychain: MutableState, storedKey: MutableState, migration: Boolean) { DatabaseUtils.ksDatabasePassword.remove() - setUseKeychain(false, useKeychain, prefs, migration) + setUseKeychain(false, useKeychain, migration) storedKey.value = false } @@ -415,15 +422,14 @@ suspend fun encryptDatabase( migration: Boolean, ): Boolean { val m = ChatModel - val prefs = ChatController.appPrefs progressIndicator.value = true return try { - prefs.encryptionStartedAt.set(Clock.System.now()) + appPrefs.encryptionStartedAt.set(Clock.System.now()) if (!m.chatDbChanged.value) { m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) } val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) - prefs.encryptionStartedAt.set(null) + appPrefs.encryptionStartedAt.set(null) val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError when { sqliteError is SQLiteError.ErrorNotADatabase -> { @@ -451,8 +457,8 @@ suspend fun encryptDatabase( resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) if (useKeychain.value) { DatabaseUtils.ksDatabasePassword.set(new) - } else if (migration) { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true) + } else { + removePassphraseFromKeyChain(useKeychain, storedKey, migration) } operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) @@ -523,7 +529,6 @@ fun PreviewDatabaseEncryptionLayout() { SimpleXTheme { DatabaseEncryptionLayout( useKeychain = remember { mutableStateOf(true) }, - prefs = AppPreferences(), chatDbEncrypted = true, currentKey = remember { mutableStateOf("") }, newKey = remember { mutableStateOf("") }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 8549f6abe2..ca0a6f2f93 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -448,6 +448,9 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState? = n progressIndicator?.value = true stopChatAsync(m) platform.androidChatStopped() + // close chat view for desktop + chatModel.chatId.value = null + ModalManager.end.closeModals() onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt index 6990a69ebd..8ad877d879 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt @@ -37,7 +37,7 @@ fun SwipeToDismissModifier( return Modifier.swipeable( state = state, anchors = anchors, - thresholds = { _, _ -> FractionalThreshold(0.5f) }, + thresholds = { _, _ -> FractionalThreshold(0.99f) }, orientation = Orientation.Horizontal, reverseDirection = isRtl, ).offset { IntOffset(state.offset.value.roundToInt(), 0) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 4c63d0a974..807b3a09c0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -39,7 +39,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) + chatModel.updateGroup(rhId = rhId, groupInfo) chatModel.chatItems.clear() chatModel.chatItemStatuses.clear() chatModel.chatId.value = groupInfo.id diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 65bd89b11b..a96cfa5a03 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.delay fun SetupDatabasePassphrase(m: ChatModel) { val progressIndicator = remember { mutableStateOf(false) } val prefs = m.controller.appPrefs - val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } @@ -58,7 +57,16 @@ fun SetupDatabasePassphrase(m: ChatModel) { prefs.storeDBPassphrase.set(false) val newKeyValue = newKey.value - val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false) + val success = encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = mutableStateOf(true), + useKeychain = mutableStateOf(false), + storedKey = mutableStateOf(true), + progressIndicator = progressIndicator, + migration = false + ) if (success) { startChat(newKeyValue) nextStep() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 76f522c614..d201ac482a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -15,6 +15,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager @@ -166,6 +167,24 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { SectionView { DisconnectButton(onClick = ::disconnectDesktop) } + + ProgressIndicator() +} + +@Composable +private fun ProgressIndicator() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 5898ccb657..61c8e1b75f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ClickableText @@ -58,6 +59,8 @@ fun NetworkAndServersView() { smpProxyFallback = smpProxyFallback, proxyPort = proxyPort, toggleSocksProxy = { enable -> + val def = NetCfg.defaults + val proxyDef = NetCfg.proxyDefaults if (enable) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.network_enable_socks), @@ -65,7 +68,19 @@ fun NetworkAndServersView() { confirmText = generalGetString(MR.strings.confirm_verb), onConfirm = { withBGApi { - val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get()) + var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get()) + if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) + } + if (conf.tcpTimeout == def.tcpTimeout) { + conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == def.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency) + } chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.setNetCfg(conf) networkUseSocksProxy.value = true @@ -80,7 +95,19 @@ fun NetworkAndServersView() { confirmText = generalGetString(MR.strings.confirm_verb), onConfirm = { withBGApi { - val conf = NetCfg.defaults + var conf = controller.getNetCfg().copy(socksProxy = null) + if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout) + } + if (conf.tcpTimeout == proxyDef.tcpTimeout) { + conf = conf.copy(tcpTimeout = def.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == proxyDef.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = def.rcvConcurrency) + } chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.setNetCfg(conf) networkUseSocksProxy.value = false 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 df43d1459f..b896c4e980 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -271,6 +271,11 @@ Server address is incompatible with network settings. Server version is incompatible with network settings. + + Wrong key or unknown file chunk address - most likely file is deleted. + File not found - most likely file was deleted or cancelled. + File server error: %1$s + Reply Share @@ -408,6 +413,8 @@ Error saving file Loading the file Please, wait while the file is being loaded from the linked mobile + File error + Temporary file error Voice message @@ -1403,6 +1410,8 @@ Database ID Debug delivery Record updated at + Message status + File status Sent at Created at Received at @@ -1411,6 +1420,8 @@ Disappears at Database ID: %d Record updated at: %s + Message status: %s + File status: %s Sent at: %s Created at: %s Received at: %s @@ -1910,6 +1921,9 @@ Connection stopped %s with the reason: %s]]> Disconnected with the reason: %s + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers. + This link was used with another mobile device, please create a new link on the desktop. + Copy error Disconnect desktop? Only one device can work at the same time Use from desktop in mobile app and scan QR code.]]> diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index a966c0a4e2..9d39753f23 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -14,8 +14,10 @@ import com.russhwolf.settings.* import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc +import kotlinx.coroutines.* import java.io.File import java.util.* +import java.util.concurrent.Executors @Composable actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = @@ -59,8 +61,11 @@ private val settingsThemesProps = Properties() .also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } } -actual val settings: Settings = PropertiesSettings(settingsProps) { withApi { settingsFile.writer().use { settingsProps.store(it, "") } } } -actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { withApi { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } } + +private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + +actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } } +actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } } actual fun windowOrientation(): WindowOrientation = if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) { diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 6fa11a1bbc..66178aa557 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=5.8-beta.5 -android.version_code=218 +android.version_name=5.8 +android.version_code=219 -desktop.version_name=5.8-beta.5 -desktop.version_code=52 +desktop.version_name=5.8 +desktop.version_code=53 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md index b9c4e74237..9e915bd3c4 100644 --- a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md +++ b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md @@ -2,10 +2,9 @@ layout: layouts/article.html title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes" date: 2024-06-04 -# previewBody: blog_previews/20240426.html -draft: true -# image: images/20240426-profile.png -# imageBottom: true +previewBody: blog_previews/20240604.html +image: images/20240604-routing.png +imageBottom: true permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html" --- @@ -13,6 +12,159 @@ permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes **Published:** June 4, 2024 -TODO +What's new in v5.8: +- [private message routing](#private-message-routing). +- [server transparency](#server-transparency). +- [protect IP address when downloading files & media](#protect-ip-address-when-downloading-files--media). +- [chat themes](#chat-themes) for better conversation privacy - in Android and desktop apps. +- [group improvements](#group-improvements) - reduced traffic and additional preferences. +- improved networking, message and file delivery. -This is a permalink for release announcement. +Also, we added Persian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## Private message routing + +### What's the problem? + + + +SimpleX network design has always been focussed on protecting user identity on the messaging protocol level - there is no user profile identifiers of any kind in the protocol design, not even random numbers or cryptographic keys. + +Until this release though, SimpleX network had no built-in protection of user transport identities - IP addresses. As previously the users could only choose which messaging relays to use to receive messages, these relays could observe the IP addresses of the senders, and if these relays were controlled by the recipients, the recipients themselves could observe them too - either by modifying server code or simply by tracking all connecting IP addresses. + +To work around this limitation, many users connected to SimpleX network relays via Tor or VPN - so that the recipients' relays could not observe IP addresses of the users when they send messages. Still, it was the most important and the most criticized limitation of SimpleX network for the users. + +### Why didn't we just embed Tor in the app? + +Tor is the best transport overlay network in existence, and it provides network anonymity for millions of Internet users. + +SimpleX Chat has many integration points with Tor: +- it allows [dual server addresses](./20220901-simplex-chat-v3.2-incognito-mode.md#using-onion-server-addresses-with-tor), when the same messaging relay can be reached both via Tor and via clearnet. +- it utilises Tor's SOCKS proxy "isolate-by-auth" feature to create a new Tor circuit for each user profile, and with an additional option - for each contact. Per-contact [transport isolation](./20230204-simplex-chat-v4-5-user-chat-profiles.md#transport-isolation) is still experimental, as it doesn't work if you connect to groups with many members, and it's only available if you enable developer tools. + +Many SimpleX network design ideas are borrowed from Tor network design: +- mitigation of [MITM attack](../docs/GLOSSARY.md#man-in-the-middle-attack) on client-server connection is done in the same way as Tor relays do it - the fingerprint of offline certificate is included in server address and validated by the client. +- the private routing itself uses the approach similar to onion routing, by adding encryption layers on each hop. +- we are also considering to implement Tor's [Proof-of-work DoS defence](https://blog.torproject.org/introducing-proof-of-work-defense-for-onion-services/) mechanism. + +So why didn't we just embed Tor into the messaging clients to provide IP address protection? + +We believe that Tor may be the wrong solution for some users for one of the reasons: +- much higher latency, error rate and resource usage. +- people who want to use Tor are better served by specialized apps, such as [Orbot](https://guardianproject.info/apps/org.torproject.android/). +- Tor usage is restricted in some networks, so it would require complex configuration in the app UI. +- some countries have legislative restrictions on Tor usage, so embedding Tor would require supporting multiple app versions, and it would leave the original problem unsolved in these countries. + +Also, while Tor solves the problem of IP address protection, it doesn't solve the problem of meta-data correlation by user's transport session. When the client connects to the messaging relays via Tor, the relays can still observe which messaging queues a user sends messages to via a single TCP connection. The client can mitigate it with per-contact transport isolation, but it uses too much traffic and battery for most users. + +So we believed we would create more value to the users of SimpleX network with private message routing. This new message routing protocol provides IP address and transport session protection out of the box, once released. It can also be extended to support delayed delivery and other functions, improving both usability and transport privacy in the future. + +At the same time, we plan to continue supporting Tor and other overlay networks. Any overlay network that supports SOCKS proxy with "isolate-by-auth" feature will work with SimpleX Chat app. + +### What is private message routing and how does it work? + +Private message routing is a major milestone for SimpleX network evolution. It is a new message routing protocol that protects both users' IP addresses and transport sessions from the messaging relays chosen by their contacts. Private message routing is, effectively, a 2-hop onion routing protocol inspired by Tor design, but with one important difference - the first (forwarding) relay is always chosen by message sender and the second (destination) - by the message recipient. In this way, neither side of the conversation can observe IP address or transport session of another. + +At the same time, the relays chosen by the sending clients to forward the messages cannot observe to which connections (messaging queues) the messages are sent, because of the additional layer of end-to-end encryption between the sender and the destination relay, similar to how onion routing works in Tor network, and also thanks to the protocol design that avoids any repeated or non-random identifiers associated with the messages, that would otherwise allow correlating the messages sent to different connections as sent by the same user. Each message forwarded to the destination relay is additionally encrypted with one-time ephemeral key, to be independent of messages sent to different connections. + +The routing protocol also prevents the possibility of MITM attack by the forwarding relay, which provides the certificate the session keys of the destination server to the sending client that are cryptographically signed by the same certificate that is included in destination server address, so the client can verify that the messages are sent to the intended destination, and not intercepted. + +The diagram below shows all the encryption layers used in private message routing: + +``` +----------------- ----------------- -- TLS -- ----------------- ----------------- +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| Sending | -- e2e -- | sender's | -- e2e -- | recipient's | -- e2e -- | Receiving | +| client | message -> | Forwarding | message -> | Destination | message -> | client | +| | -- e2e -- | relay | -- e2e -- | relay | -- e2e -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +----------------- ----------------- -- TLS -- ----------------- ----------------- +``` + +**e2e** - two end-to-end encryption layers between **sending** and **receiving** clients, one of which uses double ratchet algorithm. These encryption layers are present in the previous version of message routing protocol too. + +**s2d** - encryption between the **sending** client and recipient's **destination** relay. This new encryption layer hides the message metadata (destination connection address and message notification flag) from the forwarding relay. + +**f2d** - additional new encryption layer between **forwarding** and **destination** relays, protecting from traffic correlation in case TLS is compromised - there are no identifiers or cyphertext in common between incoming and outgoing traffic of both relays inside TLS connection. + +**d2r** - additional encryption layer between destination relay and the recipient, also protecting from traffic correlation in case TLS is compromised. + +**TLS** - TLS 1.3 transport encryption. + +For private routing to work, both the forwardig and the destination relays should support the updated messaging protocol - it is supported from v5.8 of the messaging relays. It is already released to all relays preset in the app, and available as a self-hosted server. We updated [the guide](../docs/SERVER.md) about how to host your own messaging relays. + +Because many self-hosted relays did not upgrade yet, private routing is not enabled by default. To enable it, you can open *Network & servers* settings in the app and change the settings in *Private message routing* section. We recommend setting *Private routing* option to *Unprotected* (to use it only with unknown relays and when not connecting via Tor) and *Allow downgrade* to *Yes* (so messages can still be delivered to the messaging relays that didn't upgrade yet) or to *When IP hidden* (in which case the messages will fail to deliver to unknown relays that didn't upgrade yet unless you connect to them via Tor). + +Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md). + +## Server transparency + + + +Even with very limited information available to the messaging relays, there are [several things](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#simplex-messaging-protocol-server) that would reduce users' privacy that a compromised relay can do. + +We [wrote previously](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2024-03-20-server-metadata.md) that it is important that server operators commit to running unmodified server code or disclose any code modifications, and also disclose server ownership and any other relevant information. + +While we cannot require the operators of self-hosted and private servers to disclose any information about them (apart from which server code they use - this is the requirement of the AGPLv3 license to share this information with users connecting to the server), as we add other server operators to the app, it is important for the users to have all important information about these operators and servers location. + +This server release adds server information page where all this information can be made available to the users. For example, this is the information about one of the servers preset in the app. + +The updated server guide also includes [the instruction](../docs/SERVER.md#) about how to host this page for your server. It is generated as a static page when the server starts. We recommend using Caddy webserver to serve it. + +## More new things in v5.8 + +### Protect IP address when downloading files & media + +This version added the protection of your IP address when receiving files from unknown file servers without Tor. Images and voice messages won't automatically download from unknown servers too until you tap them, and confirm that you trust the file server where they were uploaded. + +### Chat themes + + + +In Android and desktop app you can now customize how the app looks by choosing wallpapers with one of the preset themes or choose your own image as a wallpaper. + +But this feature is not only about customization - it allows to set different colors and wallpaper for different user profiles and even specific conversations. You can also choose different themes for different chat profiles. + +In case you use different identities for different conversations, it helps avoiding mistakes. + +### Group improvements + +This version adds additional group configuration options to allow sending images, files and media, and also SimpleX links only to group administrators and owners. So with this release group owners can have more control over content shared in the groups. + +We also stopped unnecessary traffic caused by the members who became inactive without leaving the groups - it should substantially reduce traffic and battery consumption to the users who send messages in large groups. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +We are prioritizing users privacy and security - it would be impossible without your support. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index a040833712..34891be6ef 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,19 @@ # Blog +Jun 4, 2024 [SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + +What's new in v5.8: +- private message routing. +- server transparency. +- protect IP address when downloading files & media. +- chat themes for better conversation privacy - in Android and desktop apps. +- group improvements - reduced traffic and additional preferences. +- improved networking, message and file delivery. + +Also, we added Persian interface language to the Android and desktop apps, thanks to our users and Weblate. + +--- + Jun 1, 2024 [Children's Safety Requires End-to-End Encryption](./20240601-children-safety-requires-e2e-encryption.md) As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve. diff --git a/blog/images/20240604-routing.png b/blog/images/20240604-routing.png new file mode 100644 index 0000000000..8fb0c821c4 Binary files /dev/null and b/blog/images/20240604-routing.png differ diff --git a/blog/images/20240604-server.png b/blog/images/20240604-server.png new file mode 100644 index 0000000000..4ab610f3b9 Binary files /dev/null and b/blog/images/20240604-server.png differ diff --git a/blog/images/20240604-theme1.png b/blog/images/20240604-theme1.png new file mode 100644 index 0000000000..e9a1422a71 Binary files /dev/null and b/blog/images/20240604-theme1.png differ diff --git a/blog/images/20240604-theme2.png b/blog/images/20240604-theme2.png new file mode 100644 index 0000000000..e7972f6e05 Binary files /dev/null and b/blog/images/20240604-theme2.png differ diff --git a/cabal.project b/cabal.project index d8752937be..ff3f05eadc 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: 4248a00a14554522f973435fd1214df790482bed + tag: 853d9bcc6ff3438f5dd4aa648103829dc8056f3c source-repository-package type: git diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 0432b0f92e..fcbe5d0446 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -7,7 +7,7 @@ revision: 11.02.2024 | Updated 23.03.2024 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.6. +The latest stable version is v5.8. You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). diff --git a/docs/SERVER.md b/docs/SERVER.md index b30d4212f5..c2cb486375 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -1,6 +1,6 @@ --- title: Hosting your own SMP Server -revision: 28.05.2024 +revision: 03.06.2024 --- | Updated 28.05.2024 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | @@ -21,6 +21,7 @@ revision: 28.05.2024 - [Tor: installation and configuration](#tor-installation-and-configuration) - [Installation for onion address](#installation-for-onion-address) - [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy) + - [Server information page](#server-information-page) - [Documentation](#documentation) - [SMP server address](#smp-server-address) - [Systemd commands](#systemd-commands) @@ -227,6 +228,44 @@ The server address above should be used in your client configuration, and if you All generated configuration, along with a description for each parameter, is available inside configuration file in `/etc/opt/simplex/smp-server.ini` for further customization. Depending on the smp-server version, the configuration file looks something like this: ```ini +[INFORMATION] +# AGPLv3 license requires that you make any source code modifications +# available to the end users of the server. +# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE +# Include correct source code URI in case the server source code is modified in any way. +# If any other information fields are present, source code property also MUST be present. + +source_code: https://github.com/simplex-chat/simplexmq + +# Declaring all below information is optional, any of these fields can be omitted. + +# Server usage conditions and amendments. +# It is recommended to use standard conditions with any amendments in a separate document. +# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md +# condition_amendments: link + +# Server location and operator. +server_country: +operator: +operator_country: +website: + +# Administrative contacts. +#admin_simplex: SimpleX address +admin_email: +# admin_pgp: +# admin_pgp_fingerprint: + +# Contacts for complaints and feedback. +# complaints_simplex: SimpleX address +complaints_email: +# complaints_pgp: +# complaints_pgp_fingerprint: + +# Hosting provider. +hosting: +hosting_country: + [STORE_LOG] # The server uses STM memory for persistence, # that will be lost on restart (e.g., as with redis). @@ -289,6 +328,21 @@ websockets: off disconnect: off # ttl: 43200 # check_interval: 3600 + +[WEB] +# Set path to generate static mini-site for server information and qr codes/links +static_path: /var/opt/simplex/www + +# Run an embedded server on this port +# Onion sites can use any port and register it in the hidden service config. +# Running on a port 80 may require setting process capabilities. +# http: 8000 + +# You can run an embedded TLS web server too if you provide port and cert and key files. +# Not required for running relay on onion address. +# https: 443 +# cert: /etc/opt/simplex/web.cert +# key: /etc/opt/simplex/web.key ``` ## Server security @@ -543,6 +597,110 @@ SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp ... ``` +## Server information page + +SMP-server versions starting from `v5.8.0` can be configured to serve Web page with server information that can include admin info, server info, provider info, etc. Run the following commands as `root` user. + +1. Add the following to your smp-server configuration (please modify fields in [INFORMATION] section to include relevant information): + + ```sh + vim /etc/opt/simplex/smp-server.ini + ``` + + ```ini + [WEB] + static_path: /var/opt/simplex/www + + [INFORMATION] + # AGPLv3 license requires that you make any source code modifications + # available to the end users of the server. + # LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE + # Include correct source code URI in case the server source code is modified in any way. + # If any other information fields are present, source code property also MUST be present. + + source_code: https://github.com/simplex-chat/simplexmq + + # Declaring all below information is optional, any of these fields can be omitted. + + # Server usage conditions and amendments. + # It is recommended to use standard conditions with any amendments in a separate document. + # usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md + # condition_amendments: link + + # Server location and operator. + server_country: + operator: + operator_country: + website: + + # Administrative contacts. + #admin_simplex: SimpleX address + admin_email: + # admin_pgp: + # admin_pgp_fingerprint: + + # Contacts for complaints and feedback. + # complaints_simplex: SimpleX address + complaints_email: + # complaints_pgp: + # complaints_pgp_fingerprint: + + # Hosting provider. + hosting: + hosting_country: + ``` + +2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: + + 1. Install the packages: + + ```sh + sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl + ``` + + 2. Install caddy gpg key for repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + ``` + + 3. Install Caddy repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list + ``` + + 4. Install Caddy: + + ```sh + sudo apt update && sudo apt install caddy + ``` + + [Full Caddy instllation instructions](https://caddyserver.com/docs/install) + +3. Replace Caddy configuration with the following (don't forget to replace ``): + + ```sh + vim /etc/caddy/Caddyfile + ``` + + ```caddy + { + root * /var/opt/simplex/www + file_server + } + ``` + +4. Enable and start Caddy service: + + ```sh + systemctl enable --now caddy + ``` + +5. Upgrade your smp-server to latest version - [Updating your smp server](#updating-your-smp-server) + +6. Access the webpage you've deployed from your browser. You should see the smp-server information that you've provided in your ini file. + ## Documentation All necessary files for `smp-server` are located in `/etc/opt/simplex/` folder. @@ -635,26 +793,69 @@ You can enable `smp-server` statistics for `Grafana` dashboard by setting value Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.log`. Fields for the `csv` file are: ```sh -fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues +fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues,msgSentNtf,msgRecvNtf,dayCountNtf,weekCountNtf,monthCountNtf,qCount,msgCount,msgExpired,qDeletedNew,qDeletedSecured,pRelays_pRequests,pRelays_pSuccesses,pRelays_pErrorsConnect,pRelays_pErrorsCompat,pRelays_pErrorsOther,pRelaysOwn_pRequests,pRelaysOwn_pSuccesses,pRelaysOwn_pErrorsConnect,pRelaysOwn_pErrorsCompat,pRelaysOwn_pErrorsOther,pMsgFwds_pRequests,pMsgFwds_pSuccesses,pMsgFwds_pErrorsConnect,pMsgFwds_pErrorsCompat,pMsgFwds_pErrorsOther,pMsgFwdsOwn_pRequests,pMsgFwdsOwn_pSuccesses,pMsgFwdsOwn_pErrorsConnect,pMsgFwdsOwn_pErrorsCompat,pMsgFwdsOwn_pErrorsOther,pMsgFwdsRecv,qSub,qSubAuth,qSubDuplicate,qSubProhibited,msgSentAuth,msgSentQuota,msgSentLarge ``` -- `fromTime` - timestamp; date and time of event - -- `qCreated` - int; created queues - -- `qSecured` - int; established queues - -- `qDeleted` - int; deleted queues - -- `msgSent` - int; sent messages - -- `msgRecv` - int; received messages - -- `dayMsgQueues` - int; active queues in a day - -- `weekMsgQueues` - int; active queues in a week - -- `monthMsgQueues` - int; active queues in a month +| Field number | Field name | Field Description | +| ------------- | ---------------------------- | -------------------------- | +| 1 | `fromTime` | Date of statistics | +| Messaging queue: | +| 2 | `qCreated` | Created | +| 3 | `qSecured` | Established | +| 4 | `qDeleted` | Deleted | +| Messages: | +| 5 | `msgSent` | Sent | +| 6 | `msgRecv` | Received | +| 7 | `dayMsgQueues` | Active queues in a day | +| 8 | `weekMsgQueues` | Active queues in a week | +| 9 | `monthMsgQueues` | Active queues in a month | +| Messages with "notification" flag | +| 10 | `msgSentNtf` | Sent | +| 11 | `msgRecvNtf` | Received | +| 12 | `dayCountNtf` | Active queues in a day | +| 13 | `weekCountNtf` | Active queues in a week | +| 14 | `monthCountNtf` | Active queues in a month | +| Additional statistics: | +| 15 | `qCount` | Stored queues | +| 16 | `msgCount` | Stored messages | +| 17 | `msgExpired` | Expired messages | +| 18 | `qDeletedNew` | New deleted queues | +| 19 | `qDeletedSecured` | Secured deleted queues | +| Requested sessions with all relays: | +| 20 | `pRelays_pRequests` | - requests | +| 21 | `pRelays_pSuccesses` | - successes | +| 22 | `pRelays_pErrorsConnect` | - connection errors | +| 23 | `pRelays_pErrorsCompat` | - compatability errors | +| 24 | `pRelays_pErrorsOther` | - other errors | +| Requested sessions with own relays: | +| 25 | `pRelaysOwn_pRequests` | - requests | +| 26 | `pRelaysOwn_pSuccesses` | - successes | +| 27 | `pRelaysOwn_pErrorsConnect` | - connection errors | +| 28 | `pRelaysOwn_pErrorsCompat` | - compatability errors | +| 29 | `pRelaysOwn_pErrorsOther` | - other errors | +| Message forwards to all relays: | +| 30 | `pMsgFwds_pRequests` | - requests | +| 31 | `pMsgFwds_pSuccesses` | - successes | +| 32 | `pMsgFwds_pErrorsConnect` | - connection errors | +| 33 | `pMsgFwds_pErrorsCompat` | - compatability errors | +| 34 | `pMsgFwds_pErrorsOther` | - other errors | +| Message forward to own relays: | +| 35 | `pMsgFwdsOwn_pRequests` | - requests | +| 36 | `pMsgFwdsOwn_pSuccesses` | - successes | +| 37 | `pMsgFwdsOwn_pErrorsConnect` | - connection errors | +| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatability errors | +| 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors | +| Received message forwards: | +| 40 | `pMsgFwdsRecv` | | +| Message queue subscribtion errors: | +| 41 | `qSub` | All | +| 42 | `qSubAuth` | Authentication erorrs | +| 43 | `qSubDuplicate` | Duplicate SUB errors | +| 44 | `qSubProhibited` | Prohibited SUB errors | +| Message errors: | +| 45 | `msgSentAuth` | Authentication errors | +| 46 | `msgSentQuota` | Quota errors | +| 47 | `msgSentLarge` | Large message errors | To import `csv` to `Grafana` one should: diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 418ecc119c..563dfc562a 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."4248a00a14554522f973435fd1214df790482bed" = "0ra09p5nm60rm6k3qks54lmy9xzkjyjna419x6cj4hgf1fgxhkp5"; + "https://github.com/simplex-chat/simplexmq.git"."853d9bcc6ff3438f5dd4aa648103829dc8056f3c" = "0r4mjwcw0wjdr97xma6s2j866n5s6rz33vgigi1vj3x74bmh7k9f"; "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 7c230b0e6d..d0eafd8927 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -93,8 +93,9 @@ import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescr import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import qualified Simplex.FileTransfer.Transport as XFTP +import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, temporaryAgentError, withLockMap) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol @@ -3369,8 +3370,8 @@ agentSubscriber = do toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing E.throwIO e where - process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () - process (corrId, entId, APC e msg) = run $ case e of + process :: (ACorrId, EntityId, AEvt) -> CM' () + process (corrId, entId, AEvt e msg) = run $ case e of SAENone -> processAgentMessageNoConn msg SAEConn -> processAgentMessage corrId entId msg SAERcvFile -> processAgentMsgRcvFile corrId entId msg @@ -3654,7 +3655,7 @@ expireChatItems user@User {userId} ttl sync = do membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m -processAgentMessage :: ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () +processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessage _ connId (DEL_RCVQ srv qId err_) = toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_ processAgentMessage _ connId DEL_CONN = @@ -3683,7 +3684,7 @@ critical a = ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing e -> throwError e -processAgentMessageNoConn :: ACommand 'Agent 'AENone -> CM () +processAgentMessageNoConn :: AEvent 'AENone -> CM () processAgentMessageNoConn = \case CONNECT p h -> hostEvent $ CRHostConnected p h DISCONNECT p h -> hostEvent $ CRHostDisconnected p h @@ -3704,7 +3705,7 @@ processAgentMessageNoConn = \case cs <- withStore' (`getConnectionsContacts` conns) toView $ event srv cs -processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () +processAgentMsgSndFile :: ACorrId -> SndFileId -> AEvent 'AESndFile -> CM () processAgentMsgSndFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ @@ -3738,11 +3739,11 @@ processAgentMsgSndFile _corrId aFileId msg = do lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) case rfds of - [] -> sendFileError "no receiver descriptions" vr ft + [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft - Just _ -> sendFileError "Prohibit chaining redirects" vr ft + Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor @@ -3787,11 +3788,15 @@ processAgentMsgSndFile _corrId aFileId msg = do pure (sndMsg, msgDeliveryId) _ -> pure () _ -> pure () -- TODO error? - SFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e - | otherwise -> - sendFileError (tshow e) vr ft + SFWARN e -> do + let err = tshow e + logWarn $ "Sent file warning: " <> err + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CRSndFileWarning user ci ft err + SFERR e -> + sendFileError (agentFileError e) (tshow e) vr ft where fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText = safeDecodeUtf8 . strEncode @@ -3809,15 +3814,27 @@ processAgentMsgSndFile _corrId aFileId msg = do case L.nonEmpty fds of Just fds' -> loopSend fds' Nothing -> pure msgDeliveryId - sendFileError :: Text -> VersionRangeChat -> FileTransferMeta -> CM () - sendFileError err vr ft = do + sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM () + sendFileError ferr err vr ft = do logError $ "Sent file error: " <> err ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSSndError + liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr) lookupChatItemByFileId db vr user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) toView $ CRSndFileError user ci ft err +agentFileError :: AgentErrorType -> FileError +agentFileError = \case + XFTP _ XFTP.AUTH -> FileErrAuth + FILE NO_FILE -> FileErrNoFile + BROKER _ e -> brokerError FileErrRelay e + e -> FileErrOther $ tshow e + where + brokerError srvErr = \case + HOST -> srvErr SrvErrHost + SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion + e -> srvErr . SrvErrOther $ tshow e + splitFileDescr :: RcvFileDescrText -> CM (NonEmpty FileDescr) splitFileDescr rfdText = do partSize <- asks $ xftpDescrPartSize . config @@ -3831,7 +3848,7 @@ splitFileDescr rfdText = do then fileDescr :| [] else fileDescr <| splitParts (partNo + 1) partSize rest -processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () +processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM () processAgentMsgRcvFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ @@ -3870,21 +3887,24 @@ processAgentMsgRcvFile _corrId aFileId msg = do lookupChatItemByFileId db vr user fileId agentXFTPDeleteRcvFile aFileId fileId toView $ maybe (CRRcvStandaloneFileComplete user fsTargetPath ft) (CRRcvFileComplete user) ci_ + RFWARN e -> do + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CRRcvFileWarning user ci e ft RFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e - | e == XFTP "" XFTP.NOT_APPROVED -> do + | e == FILE NOT_APPROVED -> do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted agentXFTPDeleteRcvFile aFileId fileId forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci | otherwise -> do ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSRcvError + liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) lookupChatItemByFileId db vr user fileId agentXFTPDeleteRcvFile aFileId fileId toView $ CRRcvFileError user ci e ft -processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () +processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. @@ -3916,7 +3936,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure $ updateEntityConnStatus acEntity connStatus Nothing -> pure acEntity - agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus + agentMsgConnStatus :: AEvent e -> Maybe ConnStatus agentMsgConnStatus = \case CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady @@ -3938,7 +3958,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processINFOpqSupport Connection {pqSupport = pq} pq' = when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change" - processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () + processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case Nothing -> case agentMsg of CONF confId pqSupport _ connInfo -> do @@ -4163,7 +4183,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () - processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () + processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of INV (ACR _ cReq) -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> @@ -4591,7 +4611,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = r n'' = Just (ci, CIRcvDecryptionError mde n'') mdeUpdatedCI _ _ = Nothing - processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM () + processSndFileConn :: AEvent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM () processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} = case agentMsg of -- SMP CONF for SndFileConnection happens for direct file protocol @@ -4639,7 +4659,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () - processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () + processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = case agentMsg of INV (ACR _ cReq) -> @@ -4722,7 +4742,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo - processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> CM () + processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of REQ invId pqSupport _ connInfo -> do ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo @@ -4795,7 +4815,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (connInactive conn) $ do quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn when (quotaErrCounter' >= quotaErrInactiveCount) $ - toView $ CRConnectionInactive connEntity True + toView $ + CRConnectionInactive connEntity True _ -> pure () continueSending :: ConnectionEntity -> Connection -> CM Bool @@ -4809,13 +4830,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- we could save command records only for agent APIs we process continuations for (INV) - withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () + withCompletedCommand :: forall e. AEntityI e => Connection -> AEvent e -> (CommandData -> CM ()) -> CM () withCompletedCommand Connection {connId} agentMsg action = do - let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg + let agentMsgTag = AEvtTag (sAEntity @e) $ aEventTag agentMsg cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId case cmdData_ of Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} - | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do + | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == AEvtTag SAEConn ERR_) -> do withStore' $ \db -> deleteCommand db user cmdId action cmdData | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId @@ -4879,14 +4900,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = BROKER _ e -> brokerError SndErrRelay e SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e - e -> SndErrOther . safeDecodeUtf8 $ strEncode e + e -> SndErrOther $ tshow e where brokerError srvErr = \case NETWORK -> SndErrExpired TIMEOUT -> SndErrExpired HOST -> srvErr SrvErrHost SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion - e -> srvErr . SrvErrOther . safeDecodeUtf8 $ strEncode e + e -> srvErr . SrvErrOther $ tshow e badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk ft err = @@ -7018,22 +7039,30 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do let redirects' = mapMaybe mapRedirectMeta $ concat redirects sndFilesAll = redirects' <> sndFiles sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll - sndFilesAll'' <- catMaybes <$> mapM sndFileDescr sndFilesAll' - let sfs = map (\(XFTPSndFile {agentSndFileId = AgentSndFileId aFileId}, sfd, _) -> (aFileId, sfd)) sndFilesAll'' - withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfs - void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, _, fId) -> fId)) sndFilesAll'' + -- while file is being prepared and uploaded, it would not have description available; + -- this partitions files into those with and without descriptions - + -- files with description are deleted remotely, files without description are deleted internally + (sfsNoDescr, sfsWithDescr) <- partitionSndDescr sndFilesAll' [] [] + withAgent' $ \a -> xftpDeleteSndFilesInternal a sfsNoDescr + withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfsWithDescr + void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, fId) -> fId)) sndFilesAll' where mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId) mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId) mapRedirectMeta _ = Nothing - sndFileDescr :: (XFTPSndFile, FileTransferId) -> CM' (Maybe (XFTPSndFile, ValidFileDescription 'FSender, FileTransferId)) - sndFileDescr (xsf@XFTPSndFile {privateSndFileDescr}, fileId) = - join <$> forM privateSndFileDescr parseSndDescr - where - parseSndDescr sfdText = + partitionSndDescr :: + [(XFTPSndFile, FileTransferId)] -> + [SndFileId] -> + [(SndFileId, ValidFileDescription 'FSender)] -> + CM' ([SndFileId], [(SndFileId, ValidFileDescription 'FSender)]) + partitionSndDescr [] filesWithoutDescr filesWithDescr = pure (filesWithoutDescr, filesWithDescr) + partitionSndDescr ((XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr}, _) : xsfs) filesWithoutDescr filesWithDescr = + case privateSndFileDescr of + Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Just sfdText -> tryChatError' (parseFileDescription sfdText) >>= \case - Left _ -> pure Nothing - Right sd -> pure $ Just (xsf, sd, fileId) + Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr) userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 43e75eca52..0a7ef021e8 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -654,6 +654,7 @@ data ChatResponse | CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CRRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer} @@ -666,6 +667,7 @@ data ChatResponse | CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]} | CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CRSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary} | CRUserProfileImage {user :: User, profile :: Profile} | CRContactAliasUpdated {user :: User, toContact :: Contact} @@ -1156,8 +1158,6 @@ data ChatErrorType | CEFileImageSize {filePath :: FilePath} | CEFileNotReceived {fileId :: FileTransferId} | CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]} - | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} - | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} | CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEInlineFileProhibited {fileId :: FileTransferId} | CEInvalidQuote diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index ea79161e06..9e0eccbbde 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -536,14 +536,16 @@ data CIFileStatus (d :: MsgDirection) where CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd CIFSSndCancelled :: CIFileStatus 'MDSnd CIFSSndComplete :: CIFileStatus 'MDSnd - CIFSSndError :: CIFileStatus 'MDSnd + CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd + CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv CIFSRcvAborted :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv - CIFSRcvError :: CIFileStatus 'MDRcv + CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv + CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd deriving instance Eq (CIFileStatus d) @@ -556,14 +558,16 @@ ciFileEnded = \case CIFSSndTransfer {} -> False CIFSSndCancelled -> True CIFSSndComplete -> True - CIFSSndError -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> False CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False CIFSRcvAborted -> True CIFSRcvCancelled -> True CIFSRcvComplete -> True - CIFSRcvError -> True + CIFSRcvError {} -> True + CIFSRcvWarning {} -> False CIFSInvalid {} -> True ciFileLoaded :: CIFileStatus d -> Bool @@ -572,14 +576,16 @@ ciFileLoaded = \case CIFSSndTransfer {} -> True CIFSSndComplete -> True CIFSSndCancelled -> True - CIFSSndError -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> True CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False CIFSRcvAborted -> False CIFSRcvCancelled -> False CIFSRcvComplete -> True - CIFSRcvError -> False + CIFSRcvError {} -> False + CIFSRcvWarning {} -> False CIFSInvalid {} -> False data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) @@ -592,14 +598,16 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total) CIFSSndCancelled -> "snd_cancelled" CIFSSndComplete -> "snd_complete" - CIFSSndError -> "snd_error" + CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr + CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr CIFSRcvInvitation -> "rcv_invitation" CIFSRcvAccepted -> "rcv_accepted" CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) CIFSRcvAborted -> "rcv_aborted" CIFSRcvComplete -> "rcv_complete" CIFSRcvCancelled -> "rcv_cancelled" - CIFSRcvError -> "rcv_error" + CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr + CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr CIFSInvalid {} -> "invalid" strP = (\(AFS _ st) -> checkDirection st) <$?> strP @@ -615,14 +623,16 @@ instance StrEncoding ACIFileStatus where "snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer "snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled "snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete - "snd_error" -> pure $ AFS SMDSnd CIFSSndError + "snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP) "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer "rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled - "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError + "rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP) _ -> fail "bad file status" progress :: (Int64 -> Int64 -> a) -> A.Parser a progress f = f <$> num <*> num <|> pure (f 0 1) @@ -633,14 +643,16 @@ data JSONCIFileStatus | JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64} | JCIFSSndCancelled | JCIFSSndComplete - | JCIFSSndError + | JCIFSSndError {sndFileError :: FileError} + | JCIFSSndWarning {sndFileError :: FileError} | JCIFSRcvInvitation | JCIFSRcvAccepted | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} | JCIFSRcvAborted | JCIFSRcvComplete | JCIFSRcvCancelled - | JCIFSRcvError + | JCIFSRcvError {rcvFileError :: FileError} + | JCIFSRcvWarning {rcvFileError :: FileError} | JCIFSInvalid {text :: Text} jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus @@ -649,14 +661,16 @@ jsonCIFileStatus = \case CIFSSndTransfer sent total -> JCIFSSndTransfer sent total CIFSSndCancelled -> JCIFSSndCancelled CIFSSndComplete -> JCIFSSndComplete - CIFSSndError -> JCIFSSndError + CIFSSndError sndFileErr -> JCIFSSndError sndFileErr + CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total CIFSRcvAborted -> JCIFSRcvAborted CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvCancelled -> JCIFSRcvCancelled - CIFSRcvError -> JCIFSRcvError + CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr + CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr CIFSInvalid text -> JCIFSInvalid text aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus @@ -665,16 +679,39 @@ aciFileStatusJSON = \case JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete - JCIFSSndError -> AFS SMDSnd CIFSSndError + JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr) + JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr) JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled - JCIFSRcvError -> AFS SMDRcv CIFSRcvError + JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr) + JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr) JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text +data FileError + = FileErrAuth + | FileErrNoFile + | FileErrRelay {srvError :: SrvError} + | FileErrOther {fileError :: Text} + deriving (Eq, Show) + +instance StrEncoding FileError where + strEncode = \case + FileErrAuth -> "auth" + FileErrNoFile -> "no_file" + FileErrRelay srvErr -> "relay " <> strEncode srvErr + FileErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "auth" -> pure FileErrAuth + "no_file" -> pure FileErrNoFile + "relay" -> FileErrRelay <$> (A.space *> strP) + "other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString + -- to conveniently read file data from db data CIFileInfo = CIFileInfo { fileId :: Int64, @@ -1208,6 +1245,8 @@ instance ChatTypeI c => ToJSON (CIMeta c d) where toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta) toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus) instance MsgDirectionI d => FromJSON (CIFileStatus d) where diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 747414af37..f441afd2b3 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -27,7 +27,6 @@ import Numeric.Natural (Natural) import Options.Applicative import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) -import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 047a9c68f7..106c4b3373 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -47,7 +47,8 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) -import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) +import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String @@ -1582,7 +1583,7 @@ instance TextEncoding CommandFunction where CFAckMessage -> "ack_message" CFDeleteConn -> "delete_conn" -commandExpectedResponse :: CommandFunction -> APartyCmdTag 'Agent +commandExpectedResponse :: CommandFunction -> AEvtTag commandExpectedResponse = \case CFCreateConnGrpMemInv -> t INV_ CFCreateConnGrpInv -> t INV_ @@ -1594,7 +1595,7 @@ commandExpectedResponse = \case CFAckMessage -> t OK_ CFDeleteConn -> t OK_ where - t = APCT SAEConn + t = AEvtTag SAEConn data CommandData = CommandData { cmdId :: CommandId, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 527386864a..8cbb772066 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -54,8 +54,9 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) -import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback) +import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -65,9 +66,9 @@ 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, safeDecodeUtf8, tshow) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) import Simplex.Messaging.Version hiding (version) -import Simplex.RemoteControl.Types (RCCtrlAddress (..)) +import Simplex.RemoteControl.Types (RCCtrlAddress (..), RCErrorType (..)) import System.Console.ANSI.Types type CurrentTime = UTCTime @@ -211,6 +212,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft CRRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] CRRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e] + CRRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e] + CRRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e] CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft @@ -222,6 +225,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRSndFileCancelledXFTP {} -> [] CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e] CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] + CRSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e] + CRSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " 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 -> [viewJSON j]) info_ @@ -338,7 +343,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] - CRRemoteCtrlStopped {} -> ["remote controller stopped"] + CRRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> @@ -1186,7 +1191,7 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)] <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] where - result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] + result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError] brokerErr = case testError of BROKER _ NETWORK -> True _ -> False @@ -1768,14 +1773,16 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize] CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"] CIFSSndComplete -> ["sending " <> fstr <> " complete"] - CIFSSndError -> ["sending " <> fstr <> " error"] + CIFSSndError sndFileErr -> ["sending " <> fstr <> " error: " <> plain (show sndFileErr)] + CIFSSndWarning sndFileErr -> ["sending " <> fstr <> " warning: " <> plain (show sndFileErr)] CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] - CIFSRcvError -> ["receiving " <> fstr <> " error"] + CIFSRcvError rcvFileErr -> ["receiving " <> fstr <> " error: " <> plain (show rcvFileErr)] + CIFSRcvWarning rcvFileErr -> ["receiving " <> fstr <> " warning: " <> plain (show rcvFileErr)] CIFSInvalid text -> [fstr <> " invalid status: " <> plain text] where fstr = fileTransferStr fileId fileName @@ -1829,7 +1836,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", "", - viewJSON WCCallAnswer {answer, iceCandidates} + viewJSON WCCallAnswer {answer, iceCandidates} ] callMediaStr :: CallType -> StyledString @@ -1900,6 +1907,12 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App | otherwise = "" showCompatible = if compatible then "" else ", " <> bold' "not compatible" +viewRemoteCtrlStopped :: RemoteCtrlStopReason -> [StyledString] +viewRemoteCtrlStopped = \case + RCSRConnectionFailed (ChatErrorAgent (RCP RCEIdentity) _) -> + ["remote controller stopped: this link was used with another controller, please create a new link on the host"] + _ -> ["remote controller stopped"] + viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString] viewChatError isCmd logLevel testView = \case ChatError err -> case err of @@ -1976,8 +1989,6 @@ viewChatError isCmd logLevel testView = \case CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"] CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"] CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs - CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] - CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] CEInvalidQuote -> ["cannot reply to this message"] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 589d880e8f..ceb3988ff9 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -131,8 +131,7 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16} testAgentCfg :: AgentConfig testAgentCfg = aCfg - { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}, - xftpNotifyErrsOnRetry = False + { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000} } testCfg :: ChatConfig diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 4241a97f43..5f2fa1ecde 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -808,7 +808,7 @@ testTestSMPServerConnection = alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" - alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK" + alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" testGetSetXFTPServers :: HasCallStack => FilePath -> IO () @@ -839,7 +839,7 @@ testTestXFTPServer = alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice <## "XFTP server test passed" alice ##> "/xftp test xftp://LcJU@localhost:7002" - alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK" + alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO () diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 572d9294a9..0016666688 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -738,7 +738,7 @@ testXFTPRcvError tmp = do _ <- getTermLine bob bob ##> "/fs 1" - bob <## "receiving file 1 (test.pdf) error" + bob <## "receiving file 1 (test.pdf) error: FileErrAuth" testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO () testXFTPCancelRcvRepeat = diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index ac6fa7b23a..3f1bad613a 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -119,7 +119,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil inv <- getTermLine desktop mobileBob ##> ("/connect remote ctrl " <> inv) mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber) - mobileBob <## "remote controller stopped" + mobileBob <## "remote controller stopped: this link was used with another controller, please create a new link on the host" -- the server remains active after rejecting invalid client mobile ##> ("/connect remote ctrl " <> inv) diff --git a/website/src/_data/glossary.json b/website/src/_data/glossary.json index 3420ba3700..a16c2b9541 100644 --- a/website/src/_data/glossary.json +++ b/website/src/_data/glossary.json @@ -59,6 +59,10 @@ "term": "Man-in-the-middle attack", "definition": "Man-in-the-middle attack" }, + { + "term": "MITM attack", + "definition": "Man-in-the-middle attack" + }, { "term": "Merkle directed acyclic graph", "definition": "Merkle directed acyclic graph" diff --git a/website/src/_includes/blog_previews/20240604.html b/website/src/_includes/blog_previews/20240604.html new file mode 100644 index 0000000000..50ae43161d --- /dev/null +++ b/website/src/_includes/blog_previews/20240604.html @@ -0,0 +1,14 @@ +

v5.8 is released:

+ +
    +
  • private message routing.
  • +
  • server transparency.
  • +
  • protect IP address when downloading files & media.
  • +
  • chat themes* for better conversation privacy.
  • +
  • group improvements - reduced traffic and additional preferences.
  • +
  • improved networking, message and file delivery.
  • +
+ +

Also, we added Persian interface language*, thanks to our users and Weblate.

+ +

* Android and desktop apps only.

\ No newline at end of file diff --git a/website/src/monerokon.html b/website/src/monerokon.html index e854065b6b..060c6883bb 100644 --- a/website/src/monerokon.html +++ b/website/src/monerokon.html @@ -2,7 +2,7 @@ layout: layouts/group_link.html title: "SimpleX Chat - MoneroKon group" description: "Join the group of attendees of Monero Konferenco 3 - Praha 2023" -groupLink: "https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F1OXnPP15cK8HAJ3YM_7UfQhlW-9WFE8P%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHf0AClqIM2SnOJ7OP06pr7UXlcnzGaBUyx3MLmRP0ko%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22CIdAO_gOEDOsW9oZrtAHiA%3D%3D%22%7D" +groupLink: "https://simplex.chat/contact/#/?v=1-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FIE3ZKT3daRLKdQg1nSXK4U1cUK4A81XQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAN2vLBKKQiTG58nokhiBIpqvLTyfeyey6UbaFGy4cYH8%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%227LTn4BEWw4bD9Gs8snVEJA%3D%3D%22%7D" groupLinkText: Open MoneroKon group link templateEngineOverride: njk ---- +--- \ No newline at end of file